| // 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: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' as globals; |
| import 'plugins.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 FlutterManifest createFromPath(String path) { |
| if (path == null || !globals.fs.isFileSync(path)) { |
| return _createFromYaml(null); |
| } |
| final String manifest = globals.fs.file(path).readAsStringSync(); |
| return createFromString(manifest); |
| } |
| |
| /// Returns null on missing or invalid manifest |
| @visibleForTesting |
| static FlutterManifest createFromString(String manifest) { |
| return _createFromYaml(loadYaml(manifest) as YamlMap); |
| } |
| |
| static FlutterManifest _createFromYaml(YamlMap yamlDocument) { |
| final FlutterManifest pubspec = FlutterManifest._(); |
| if (yamlDocument != null && !_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'] as Map<dynamic, dynamic>; |
| 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'] as String ?? ''; |
| |
| // 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) { |
| globals.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); |
| } |
| 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'] as bool ?? false; |
| } |
| |
| /// True if this Flutter module should use AndroidX dependencies. |
| /// |
| /// If false the deprecated Android Support library will be used. |
| bool get usesAndroidX { |
| if (_flutterDescriptor.containsKey('module')) { |
| return _flutterDescriptor['module']['androidX'] as bool; |
| } |
| return 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'] as String; |
| } |
| if (isPlugin) { |
| final YamlMap plugin = _flutterDescriptor['plugin'] as YamlMap; |
| if (plugin.containsKey('platforms')) { |
| return plugin['platforms']['android']['package'] as String; |
| } else { |
| return plugin['androidPackage'] as String; |
| } |
| } |
| 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'] as String; |
| } |
| 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'] as List<dynamic>; |
| return fontList == null |
| ? const <Map<String, dynamic>>[] |
| : fontList.map<Map<String, dynamic>>(castStringKeyedMap).toList(); |
| } |
| |
| List<Uri> get assets => _assets ??= _computeAssets(); |
| List<Uri> _assets; |
| List<Uri> _computeAssets() { |
| final List<dynamic> assets = _flutterDescriptor['assets'] as List<dynamic>; |
| if (assets == null) { |
| return const <Uri>[]; |
| } |
| final List<Uri> results = <Uri>[]; |
| for (final Object asset in assets) { |
| if (asset is! String || asset == null || asset == '') { |
| globals.printError('Asset manifest contains a null or empty uri.'); |
| continue; |
| } |
| final String stringAsset = asset as String; |
| try { |
| results.add(Uri(pathSegments: stringAsset.split('/'))); |
| } on FormatException { |
| globals.printError('Asset manifest contains invalid uri: $asset.'); |
| } |
| } |
| return results; |
| } |
| |
| 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 (final Map<String, dynamic> fontFamily in _rawFontsDescriptor) { |
| final YamlList fontFiles = fontFamily['fonts'] as YamlList; |
| final String familyName = fontFamily['family'] as String; |
| if (familyName == null) { |
| globals.printError('Warning: Missing family name for font.', emphasis: true); |
| continue; |
| } |
| if (fontFiles == null) { |
| globals.printError('Warning: No fonts specified for font $familyName', emphasis: true); |
| continue; |
| } |
| |
| final List<FontAsset> fontAssets = <FontAsset>[]; |
| for (final Map<dynamic, dynamic> fontFile in fontFiles.cast<Map<dynamic, dynamic>>()) { |
| final String asset = fontFile['asset'] as String; |
| if (asset == null) { |
| globals.printError('Warning: Missing asset in fonts for $familyName', emphasis: true); |
| continue; |
| } |
| |
| fontAssets.add(FontAsset( |
| Uri.parse(asset), |
| weight: fontFile['weight'] as int, |
| style: fontFile['style'] as String, |
| )); |
| } |
| if (fontAssets.isNotEmpty) { |
| fonts.add(Font(fontFamily['family'] as String, 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 globals.fs.path.join( |
| globals.fs.path.absolute(Cache.flutterRoot), 'packages', 'flutter_tools', 'schema', |
| ); |
| } |
| |
| @visibleForTesting |
| String buildSchemaPath(FileSystem fs) { |
| return globals.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 dependencies on packages for simple validation. |
| bool _validate(YamlMap manifest) { |
| 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 as String) { |
| 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}.'); |
| } else { |
| _validateFlutter(kvp.value as YamlMap, errors); |
| } |
| break; |
| default: |
| // additionalProperties are allowed. |
| break; |
| } |
| } |
| |
| if (errors.isNotEmpty) { |
| globals.printStatus('Error detected in pubspec.yaml:', emphasis: true); |
| globals.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 as String) { |
| 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}).'); |
| } else { |
| _validateFonts(kvp.value as YamlList, 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['androidX'] != null && kvp.value['androidX'] is! bool) { |
| errors.add('The "androidX" value must be a bool if set.'); |
| } |
| 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 || kvp.value == null) { |
| errors.add('Expected "${kvp.key}" to be an object, but got ${kvp.value} (${kvp.value.runtimeType}).'); |
| break; |
| } |
| final List<String> pluginErrors = Plugin.validatePluginYaml(kvp.value as YamlMap); |
| errors.addAll(pluginErrors); |
| 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 dynamic fontListEntry in fonts) { |
| if (fontListEntry is! YamlMap) { |
| errors.add('Unexpected child "$fontListEntry" found under "fonts". Expected a map.'); |
| continue; |
| } |
| final YamlMap fontMap = fontListEntry as YamlMap; |
| for (final 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) { |
| continue; |
| } else if (fontMap['fonts'] is! YamlList) { |
| errors.add('Expected "fonts" to either be null or a list.'); |
| continue; |
| } |
| for (final dynamic fontListItem in fontMap['fonts']) { |
| if (fontListItem is! YamlMap) { |
| errors.add('Expected "fonts" to be a list of maps.'); |
| continue; |
| } |
| final YamlMap fontMapList = fontListItem as YamlMap; |
| for (final MapEntry<dynamic, dynamic> kvp in fontMapList.entries) { |
| if (kvp.key is! String) { |
| errors.add('Expected "${kvp.key}" under "fonts" to be a string.'); |
| } |
| switch(kvp.key as String) { |
| 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; |
| } |
| } |
| } |
| } |
| } |