flutter/packages/flutter_tools/lib/src/flutter_manifest.dart
Ian Hickson 449f4a6673
License update (#45373)
* Update project.pbxproj files to say Flutter rather than Chromium

Also, the templates now have an empty organization so that we don't cause people to give their apps a Flutter copyright.

* Update the copyright notice checker to require a standard notice on all files

* Update copyrights on Dart files. (This was a mechanical commit.)

* Fix weird license headers on Dart files that deviate from our conventions; relicense Shrine.

Some were already marked "The Flutter Authors", not clear why. Their
dates have been normalized. Some were missing the blank line after the
license. Some were randomly different in trivial ways for no apparent
reason (e.g. missing the trailing period).

* Clean up the copyrights in non-Dart files. (Manual edits.)

Also, make sure templates don't have copyrights.

* Fix some more ORGANIZATIONNAMEs
2019-11-27 15:04:02 -08:00

480 lines
15 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/user_messages.dart';
import 'base/utils.dart';
import 'cache.dart';
import 'globals.dart';
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 || !fs.isFileSync(path)) {
return _createFromYaml(null);
}
final String manifest = 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) {
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 (Object asset in assets) {
if (asset is! String || asset == null || asset == '') {
printError('Asset manifest contains a null or empty uri.');
continue;
}
final String stringAsset = asset as String;
try {
results.add(Uri.parse(Uri.encodeFull(stringAsset)));
} on FormatException {
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 (Map<String, dynamic> fontFamily in _rawFontsDescriptor) {
final YamlList fontFiles = fontFamily['fonts'] as YamlList;
final String familyName = fontFamily['family'] as String;
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<FontAsset> fontAssets = <FontAsset>[];
for (Map<dynamic, dynamic> fontFile in fontFiles.cast<Map<dynamic, dynamic>>()) {
final String asset = fontFile['asset'] as String;
if (asset == null) {
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 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',
);
}
/// 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}.');
}
_validateFlutter(kvp.value as YamlMap, errors);
break;
default:
// additionalProperties are allowed.
break;
}
}
if (errors.isNotEmpty) {
printStatus('Error detected in pubspec.yaml:', emphasis: true);
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 (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;
}
}
}
}
}