// 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 'dart:convert' as convert; import 'package:json_schema/json_schema.dart'; import 'package:meta/meta.dart'; import 'package:yaml/yaml.dart'; import 'base/file_system.dart'; import 'base/utils.dart'; import 'cache.dart'; import 'globals.dart'; final RegExp _versionPattern = new RegExp(r'^(\d+)(\.(\d+)(\.(\d+))?)?(\+(\d+))?$'); /// A wrapper around the `flutter` section in the `pubspec.yaml` file. class FlutterManifest { FlutterManifest._(); /// Returns an empty manifest. static FlutterManifest empty() { final FlutterManifest manifest = new FlutterManifest._(); manifest._descriptor = const {}; manifest._flutterDescriptor = const {}; return manifest; } /// Returns null on invalid manifest. Returns empty manifest on missing file. static Future 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 createFromString(String manifest) async { return _createFromYaml(loadYaml(manifest)); } static Future _createFromYaml(dynamic yamlDocument) async { final FlutterManifest pubspec = new FlutterManifest._(); if (yamlDocument != null && !await _validate(yamlDocument)) return null; final Map yamlMap = yamlDocument; if (yamlMap != null) { pubspec._descriptor = yamlMap.cast(); } else { pubspec._descriptor = {}; } final Map flutterMap = pubspec._descriptor['flutter']; if (flutterMap != null) { pubspec._flutterDescriptor = flutterMap.cast(); } else { pubspec._flutterDescriptor = {}; } return pubspec; } /// A map representation of the entire `pubspec.yaml` file. Map _descriptor; /// A map representation of the `flutter` section in the `pubspec.yaml` file. Map _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'] ?? ''; /// 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 version = _descriptor['version']?.toString(); if (version != null && _versionPattern.hasMatch(version)) return version; else return null; } /// 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. int get buildNumber { if (appVersion != null && appVersion.contains('+')) { final String value = appVersion.split('+')?.elementAt(1); return value == null ? null : int.tryParse(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. /// /// 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> get fontsDescriptor { final List fontList = _flutterDescriptor['fonts']; return fontList == null ? const >[] : fontList.map>(castStringKeyedMap).toList(); } List get assets { final List assets = _flutterDescriptor['assets']; if (assets == null) { return const []; } return assets .cast() .map(Uri.encodeFull) ?.map(Uri.parse) ?.toList(); } List _fonts; List get fonts { _fonts ??= _extractFonts(); return _fonts; } List _extractFonts() { if (!_flutterDescriptor.containsKey('fonts')) return []; final List fonts = []; for (Map fontFamily in fontsDescriptor) { final List 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 fontAssets = []; for (Map fontFile in fontFiles) { final String asset = fontFile['asset']; if (asset == null) { printError('Warning: Missing asset in fonts for $familyName', emphasis: true); continue; } fontAssets.add(new FontAsset( Uri.parse(asset), weight: fontFile['weight'], style: fontFile['style'], )); } if (fontAssets.isNotEmpty) fonts.add(new 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 fontAssets; Map get descriptor { return { 'family': familyName, 'fonts': fontAssets.map((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 get descriptor { final Map descriptor = {}; 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', ); } Future _validate(dynamic manifest) async { final String schemaPath = buildSchemaPath(fs); final String schemaData = fs.file(schemaPath).readAsStringSync(); final Schema schema = await Schema.createSchema( convert.json.decode(schemaData)); final Validator validator = new Validator(schema); if (validator.validate(manifest)) { return true; } else { printStatus('Error detected in pubspec.yaml:', emphasis: true); printError(validator.errors.join('\n')); return false; } }