1// Copyright 2017 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'package:meta/meta.dart'; 6import 'package:pub_semver/pub_semver.dart'; 7import 'package:yaml/yaml.dart'; 8 9import 'base/file_system.dart'; 10import 'base/user_messages.dart'; 11import 'base/utils.dart'; 12import 'cache.dart'; 13import 'globals.dart'; 14 15/// A wrapper around the `flutter` section in the `pubspec.yaml` file. 16class FlutterManifest { 17 FlutterManifest._(); 18 19 /// Returns an empty manifest. 20 static FlutterManifest empty() { 21 final FlutterManifest manifest = FlutterManifest._(); 22 manifest._descriptor = const <String, dynamic>{}; 23 manifest._flutterDescriptor = const <String, dynamic>{}; 24 return manifest; 25 } 26 27 /// Returns null on invalid manifest. Returns empty manifest on missing file. 28 static FlutterManifest createFromPath(String path) { 29 if (path == null || !fs.isFileSync(path)) 30 return _createFromYaml(null); 31 final String manifest = fs.file(path).readAsStringSync(); 32 return createFromString(manifest); 33 } 34 35 /// Returns null on missing or invalid manifest 36 @visibleForTesting 37 static FlutterManifest createFromString(String manifest) { 38 return _createFromYaml(loadYaml(manifest)); 39 } 40 41 static FlutterManifest _createFromYaml(dynamic yamlDocument) { 42 final FlutterManifest pubspec = FlutterManifest._(); 43 if (yamlDocument != null && !_validate(yamlDocument)) 44 return null; 45 46 final Map<dynamic, dynamic> yamlMap = yamlDocument; 47 if (yamlMap != null) { 48 pubspec._descriptor = yamlMap.cast<String, dynamic>(); 49 } else { 50 pubspec._descriptor = <String, dynamic>{}; 51 } 52 53 final Map<dynamic, dynamic> flutterMap = pubspec._descriptor['flutter']; 54 if (flutterMap != null) { 55 pubspec._flutterDescriptor = flutterMap.cast<String, dynamic>(); 56 } else { 57 pubspec._flutterDescriptor = <String, dynamic>{}; 58 } 59 60 return pubspec; 61 } 62 63 /// A map representation of the entire `pubspec.yaml` file. 64 Map<String, dynamic> _descriptor; 65 66 /// A map representation of the `flutter` section in the `pubspec.yaml` file. 67 Map<String, dynamic> _flutterDescriptor; 68 69 /// True if the `pubspec.yaml` file does not exist. 70 bool get isEmpty => _descriptor.isEmpty; 71 72 /// The string value of the top-level `name` property in the `pubspec.yaml` file. 73 String get appName => _descriptor['name'] ?? ''; 74 75 // Flag to avoid printing multiple invalid version messages. 76 bool _hasShowInvalidVersionMsg = false; 77 78 /// The version String from the `pubspec.yaml` file. 79 /// Can be null if it isn't set or has a wrong format. 80 String get appVersion { 81 final String verStr = _descriptor['version']?.toString(); 82 if (verStr == null) { 83 return null; 84 } 85 86 Version version; 87 try { 88 version = Version.parse(verStr); 89 } on Exception { 90 if (!_hasShowInvalidVersionMsg) { 91 printStatus(userMessages.invalidVersionSettingHintMessage(verStr), emphasis: true); 92 _hasShowInvalidVersionMsg = true; 93 } 94 } 95 return version?.toString(); 96 } 97 98 /// The build version name from the `pubspec.yaml` file. 99 /// Can be null if version isn't set or has a wrong format. 100 String get buildName { 101 if (appVersion != null && appVersion.contains('+')) 102 return appVersion.split('+')?.elementAt(0); 103 else 104 return appVersion; 105 } 106 107 /// The build version number from the `pubspec.yaml` file. 108 /// Can be null if version isn't set or has a wrong format. 109 String get buildNumber { 110 if (appVersion != null && appVersion.contains('+')) { 111 final String value = appVersion.split('+')?.elementAt(1); 112 return value; 113 } else { 114 return null; 115 } 116 } 117 118 bool get usesMaterialDesign { 119 return _flutterDescriptor['uses-material-design'] ?? false; 120 } 121 122 /// True if this Flutter module should use AndroidX dependencies. 123 /// 124 /// If false the deprecated Android Support library will be used. 125 bool get usesAndroidX { 126 return _flutterDescriptor['module']['androidX'] ?? false; 127 } 128 129 /// True if this manifest declares a Flutter module project. 130 /// 131 /// A Flutter project is considered a module when it has a `module:` 132 /// descriptor. A Flutter module project supports integration into an 133 /// existing host app, and has managed platform host code. 134 /// 135 /// Such a project can be created using `flutter create -t module`. 136 bool get isModule => _flutterDescriptor.containsKey('module'); 137 138 /// True if this manifest declares a Flutter plugin project. 139 /// 140 /// A Flutter project is considered a plugin when it has a `plugin:` 141 /// descriptor. A Flutter plugin project wraps custom Android and/or 142 /// iOS code in a Dart interface for consumption by other Flutter app 143 /// projects. 144 /// 145 /// Such a project can be created using `flutter create -t plugin`. 146 bool get isPlugin => _flutterDescriptor.containsKey('plugin'); 147 148 /// Returns the Android package declared by this manifest in its 149 /// module or plugin descriptor. Returns null, if there is no 150 /// such declaration. 151 String get androidPackage { 152 if (isModule) 153 return _flutterDescriptor['module']['androidPackage']; 154 if (isPlugin) 155 return _flutterDescriptor['plugin']['androidPackage']; 156 return null; 157 } 158 159 /// Returns the iOS bundle identifier declared by this manifest in its 160 /// module descriptor. Returns null if there is no such declaration. 161 String get iosBundleIdentifier { 162 if (isModule) 163 return _flutterDescriptor['module']['iosBundleIdentifier']; 164 return null; 165 } 166 167 List<Map<String, dynamic>> get fontsDescriptor { 168 return fonts.map((Font font) => font.descriptor).toList(); 169 } 170 171 List<Map<String, dynamic>> get _rawFontsDescriptor { 172 final List<dynamic> fontList = _flutterDescriptor['fonts']; 173 return fontList == null 174 ? const <Map<String, dynamic>>[] 175 : fontList.map<Map<String, dynamic>>(castStringKeyedMap).toList(); 176 } 177 178 List<Uri> get assets { 179 final List<dynamic> assets = _flutterDescriptor['assets']; 180 if (assets == null) { 181 return const <Uri>[]; 182 } 183 return assets 184 .cast<String>() 185 .map<String>(Uri.encodeFull) 186 ?.map<Uri>(Uri.parse) 187 ?.toList(); 188 } 189 190 List<Font> _fonts; 191 192 List<Font> get fonts { 193 _fonts ??= _extractFonts(); 194 return _fonts; 195 } 196 197 List<Font> _extractFonts() { 198 if (!_flutterDescriptor.containsKey('fonts')) 199 return <Font>[]; 200 201 final List<Font> fonts = <Font>[]; 202 for (Map<String, dynamic> fontFamily in _rawFontsDescriptor) { 203 final List<dynamic> fontFiles = fontFamily['fonts']; 204 final String familyName = fontFamily['family']; 205 if (familyName == null) { 206 printError('Warning: Missing family name for font.', emphasis: true); 207 continue; 208 } 209 if (fontFiles == null) { 210 printError('Warning: No fonts specified for font $familyName', emphasis: true); 211 continue; 212 } 213 214 final List<FontAsset> fontAssets = <FontAsset>[]; 215 for (Map<dynamic, dynamic> fontFile in fontFiles) { 216 final String asset = fontFile['asset']; 217 if (asset == null) { 218 printError('Warning: Missing asset in fonts for $familyName', emphasis: true); 219 continue; 220 } 221 222 fontAssets.add(FontAsset( 223 Uri.parse(asset), 224 weight: fontFile['weight'], 225 style: fontFile['style'], 226 )); 227 } 228 if (fontAssets.isNotEmpty) 229 fonts.add(Font(fontFamily['family'], fontAssets)); 230 } 231 return fonts; 232 } 233} 234 235class Font { 236 Font(this.familyName, this.fontAssets) 237 : assert(familyName != null), 238 assert(fontAssets != null), 239 assert(fontAssets.isNotEmpty); 240 241 final String familyName; 242 final List<FontAsset> fontAssets; 243 244 Map<String, dynamic> get descriptor { 245 return <String, dynamic>{ 246 'family': familyName, 247 'fonts': fontAssets.map<Map<String, dynamic>>((FontAsset a) => a.descriptor).toList(), 248 }; 249 } 250 251 @override 252 String toString() => '$runtimeType(family: $familyName, assets: $fontAssets)'; 253} 254 255class FontAsset { 256 FontAsset(this.assetUri, {this.weight, this.style}) 257 : assert(assetUri != null); 258 259 final Uri assetUri; 260 final int weight; 261 final String style; 262 263 Map<String, dynamic> get descriptor { 264 final Map<String, dynamic> descriptor = <String, dynamic>{}; 265 if (weight != null) 266 descriptor['weight'] = weight; 267 268 if (style != null) 269 descriptor['style'] = style; 270 271 descriptor['asset'] = assetUri.path; 272 return descriptor; 273 } 274 275 @override 276 String toString() => '$runtimeType(asset: ${assetUri.path}, weight; $weight, style: $style)'; 277} 278 279@visibleForTesting 280String buildSchemaDir(FileSystem fs) { 281 return fs.path.join( 282 fs.path.absolute(Cache.flutterRoot), 'packages', 'flutter_tools', 'schema', 283 ); 284} 285 286@visibleForTesting 287String buildSchemaPath(FileSystem fs) { 288 return fs.path.join( 289 buildSchemaDir(fs), 290 'pubspec_yaml.json', 291 ); 292} 293 294/// This method should be kept in sync with the schema in 295/// `$FLUTTER_ROOT/packages/flutter_tools/schema/pubspec_yaml.json`, 296/// but avoid introducing depdendencies on packages for simple validation. 297bool _validate(YamlMap manifest) { 298 final List<String> errors = <String>[]; 299 for (final MapEntry<dynamic, dynamic> kvp in manifest.entries) { 300 if (kvp.key is! String) { 301 errors.add('Expected YAML key to be a a string, but got ${kvp.key}.'); 302 continue; 303 } 304 switch (kvp.key) { 305 case 'name': 306 if (kvp.value is! String) { 307 errors.add('Expected "${kvp.key}" to be a string, but got ${kvp.value}.'); 308 } 309 break; 310 case 'flutter': 311 if (kvp.value == null) { 312 continue; 313 } 314 if (kvp.value is! YamlMap) { 315 errors.add('Expected "${kvp.key}" section to be an object or null, but got ${kvp.value}.'); 316 } 317 _validateFlutter(kvp.value, errors); 318 break; 319 default: 320 // additionalProperties are allowed. 321 break; 322 } 323 } 324 325 if (errors.isNotEmpty) { 326 printStatus('Error detected in pubspec.yaml:', emphasis: true); 327 printError(errors.join('\n')); 328 return false; 329 } 330 331 return true; 332} 333 334void _validateFlutter(YamlMap yaml, List<String> errors) { 335 if (yaml == null || yaml.entries == null) { 336 return; 337 } 338 for (final MapEntry<dynamic, dynamic> kvp in yaml.entries) { 339 if (kvp.key is! String) { 340 errors.add('Expected YAML key to be a a string, but got ${kvp.key} (${kvp.value.runtimeType}).'); 341 continue; 342 } 343 switch (kvp.key) { 344 case 'uses-material-design': 345 if (kvp.value is! bool) { 346 errors.add('Expected "${kvp.key}" to be a bool, but got ${kvp.value} (${kvp.value.runtimeType}).'); 347 } 348 break; 349 case 'assets': 350 case 'services': 351 if (kvp.value is! YamlList || kvp.value[0] is! String) { 352 errors.add('Expected "${kvp.key}" to be a list, but got ${kvp.value} (${kvp.value.runtimeType}).'); 353 } 354 break; 355 case 'fonts': 356 if (kvp.value is! YamlList || kvp.value[0] is! YamlMap) { 357 errors.add('Expected "${kvp.key}" to be a list, but got ${kvp.value} (${kvp.value.runtimeType}).'); 358 } else { 359 _validateFonts(kvp.value, errors); 360 } 361 break; 362 case 'module': 363 if (kvp.value is! YamlMap) { 364 errors.add('Expected "${kvp.key}" to be an object, but got ${kvp.value} (${kvp.value.runtimeType}).'); 365 } 366 367 if (kvp.value['androidX'] != null && kvp.value['androidX'] is! bool) { 368 errors.add('The "androidX" value must be a bool if set.'); 369 } 370 if (kvp.value['androidPackage'] != null && kvp.value['androidPackage'] is! String) { 371 errors.add('The "androidPackage" value must be a string if set.'); 372 } 373 if (kvp.value['iosBundleIdentifier'] != null && kvp.value['iosBundleIdentifier'] is! String) { 374 errors.add('The "iosBundleIdentifier" section must be a string if set.'); 375 } 376 break; 377 case 'plugin': 378 if (kvp.value is! YamlMap) { 379 errors.add('Expected "${kvp.key}" to be an object, but got ${kvp.value} (${kvp.value.runtimeType}).'); 380 } 381 if (kvp.value['androidPackage'] != null && kvp.value['androidPackage'] is! String) { 382 errors.add('The "androidPackage" must either be null or a string.'); 383 } 384 if (kvp.value['iosPrefix'] != null && kvp.value['iosPrefix'] is! String) { 385 errors.add('The "iosPrefix" must either be null or a string.'); 386 } 387 if (kvp.value['macosPrefix'] != null && kvp.value['macosPrefix'] is! String) { 388 errors.add('The "macosPrefix" must either be null or a string.'); 389 } 390 if (kvp.value['pluginClass'] != null && kvp.value['pluginClass'] is! String) { 391 errors.add('The "pluginClass" must either be null or a string..'); 392 } 393 break; 394 default: 395 errors.add('Unexpected child "${kvp.key}" found under "flutter".'); 396 break; 397 } 398 } 399} 400 401void _validateFonts(YamlList fonts, List<String> errors) { 402 if (fonts == null) { 403 return; 404 } 405 const Set<int> fontWeights = <int>{ 406 100, 200, 300, 400, 500, 600, 700, 800, 900, 407 }; 408 for (final dynamic fontListEntry in fonts) { 409 if (fontListEntry is! YamlMap) { 410 errors.add('Unexpected child "$fontListEntry" found under "fonts". Expected a map.'); 411 continue; 412 } 413 final YamlMap fontMap = fontListEntry; 414 for (dynamic key in fontMap.keys.where((dynamic key) => key != 'family' && key != 'fonts')) { 415 errors.add('Unexpected child "$key" found under "fonts".'); 416 } 417 if (fontMap['family'] != null && fontMap['family'] is! String) { 418 errors.add('Font family must either be null or a String.'); 419 } 420 if (fontMap['fonts'] == null) { 421 continue; 422 } else if (fontMap['fonts'] is! YamlList) { 423 errors.add('Expected "fonts" to either be null or a list.'); 424 continue; 425 } 426 for (final YamlMap fontListItem in fontMap['fonts']) { 427 for (final MapEntry<dynamic, dynamic> kvp in fontListItem.entries) { 428 if (kvp.key is! String) { 429 errors.add('Expected "${kvp.key}" under "fonts" to be a string.'); 430 } 431 switch(kvp.key) { 432 case 'asset': 433 if (kvp.value is! String) { 434 errors.add('Expected font asset ${kvp.value} ((${kvp.value.runtimeType})) to be a string.'); 435 } 436 break; 437 case 'weight': 438 if (!fontWeights.contains(kvp.value)) { 439 errors.add('Invalid value ${kvp.value} ((${kvp.value.runtimeType})) for font -> weight.'); 440 } 441 break; 442 case 'style': 443 if (kvp.value != 'normal' && kvp.value != 'italic') { 444 errors.add('Invalid value ${kvp.value} ((${kvp.value.runtimeType})) for font -> style.'); 445 } 446 break; 447 default: 448 errors.add('Unexpected key ${kvp.key} ((${kvp.value.runtimeType})) under font.'); 449 break; 450 } 451 } 452 } 453 } 454} 455