// 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/logger.dart'; import 'base/user_messages.dart'; import 'base/utils.dart'; import 'cache.dart'; import 'plugins.dart'; /// A wrapper around the `flutter` section in the `pubspec.yaml` file. class FlutterManifest { FlutterManifest._(this._logger); /// Returns an empty manifest. factory FlutterManifest.empty({ @required Logger logger }) { final FlutterManifest manifest = FlutterManifest._(logger); 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, { @required FileSystem fileSystem, @required Logger logger, }) { if (path == null || !fileSystem.isFileSync(path)) { return _createFromYaml(null, logger); } final String manifest = fileSystem.file(path).readAsStringSync(); return FlutterManifest.createFromString(manifest, logger: logger); } /// Returns null on missing or invalid manifest @visibleForTesting static FlutterManifest createFromString(String manifest, { @required Logger logger }) { return _createFromYaml(loadYaml(manifest) as YamlMap, logger); } static FlutterManifest _createFromYaml(YamlMap yamlDocument, Logger logger) { final FlutterManifest pubspec = FlutterManifest._(logger); if (yamlDocument != null && !_validate(yamlDocument, logger)) { 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; } final Logger _logger; /// 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) { _logger.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')) { final YamlMap platforms = plugin['platforms'] as YamlMap; if (platforms.containsKey('android')) { return 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 == '') { _logger.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 { _logger.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) { _logger.printError('Warning: Missing family name for font.', emphasis: true); continue; } if (fontFiles == null) { _logger.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) { _logger.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 fileSystem) { return fileSystem.path.join( fileSystem.path.absolute(Cache.flutterRoot), 'packages', 'flutter_tools', 'schema', ); } @visibleForTesting String buildSchemaPath(FileSystem fileSystem) { return fileSystem.path.join( buildSchemaDir(fileSystem), '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, Logger logger) { 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 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) { logger.printStatus('Error detected in pubspec.yaml:', emphasis: true); logger.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 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; } } } } }