mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
490 lines
16 KiB
Dart
490 lines
16 KiB
Dart
// 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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|