mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

This PR adds a new flag `default-flavor` in the `flutter` section of `pubspec.yaml`. It allows developers of multi-flavor android apps to specify a default flavor to be used for `flutter run`, `flutter build` etc. Using `flutter run` on flavored apps already works without specifying `--flavor` already works on iOS (it defaults to the `runner` schema), so I (and others in #22856) figured this would be nice to have. fixes #22856
948 lines
31 KiB
Dart
948 lines
31 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/deferred_component.dart';
|
|
import 'base/file_system.dart';
|
|
import 'base/logger.dart';
|
|
import 'base/utils.dart';
|
|
import 'globals.dart' as globals;
|
|
import 'plugins.dart';
|
|
|
|
/// Whether or not Impeller Scene 3D model import is enabled.
|
|
const bool kIs3dSceneSupported = true;
|
|
|
|
const Set<String> _kValidPluginPlatforms = <String>{
|
|
'android', 'ios', 'web', 'windows', 'linux', 'macos',
|
|
};
|
|
|
|
/// A wrapper around the `flutter` section in the `pubspec.yaml` file.
|
|
class FlutterManifest {
|
|
FlutterManifest._({required Logger logger}) : _logger = logger;
|
|
|
|
/// Returns an empty manifest.
|
|
factory FlutterManifest.empty({ required Logger logger }) = FlutterManifest._;
|
|
|
|
/// Returns null on invalid manifest. Returns empty manifest on missing file.
|
|
static FlutterManifest? createFromPath(String path, {
|
|
required FileSystem fileSystem,
|
|
required Logger logger,
|
|
}) {
|
|
if (!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), logger);
|
|
}
|
|
|
|
static FlutterManifest? _createFromYaml(Object? yamlDocument, Logger logger) {
|
|
if (yamlDocument != null && !_validate(yamlDocument, logger)) {
|
|
return null;
|
|
}
|
|
|
|
final FlutterManifest pubspec = FlutterManifest._(logger: logger);
|
|
final Map<Object?, Object?>? yamlMap = yamlDocument as YamlMap?;
|
|
if (yamlMap != null) {
|
|
pubspec._descriptor = yamlMap.cast<String, Object?>();
|
|
}
|
|
|
|
final Map<Object?, Object?>? flutterMap = pubspec._descriptor['flutter'] as Map<Object?, Object?>?;
|
|
if (flutterMap != null) {
|
|
pubspec._flutterDescriptor = flutterMap.cast<String, Object?>();
|
|
}
|
|
|
|
return pubspec;
|
|
}
|
|
|
|
final Logger _logger;
|
|
|
|
/// A map representation of the entire `pubspec.yaml` file.
|
|
Map<String, Object?> _descriptor = <String, Object?>{};
|
|
|
|
/// A map representation of the `flutter` section in the `pubspec.yaml` file.
|
|
Map<String, Object?> _flutterDescriptor = <String, Object?>{};
|
|
|
|
Map<String, Object?> get flutterDescriptor => _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? ?? '';
|
|
|
|
/// Contains the name of the dependencies.
|
|
/// These are the keys specified in the `dependency` map.
|
|
Set<String> get dependencies {
|
|
final YamlMap? dependencies = _descriptor['dependencies'] as YamlMap?;
|
|
return dependencies != null ? <String>{...dependencies.keys.cast<String>()} : <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(globals.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 {
|
|
final String? version = appVersion;
|
|
if (version != null && version.contains('+')) {
|
|
return version.split('+').elementAt(0);
|
|
}
|
|
return version;
|
|
}
|
|
|
|
/// 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 {
|
|
final String? version = appVersion;
|
|
if (version != null && version.contains('+')) {
|
|
final String value = version.split('+').elementAt(1);
|
|
return value;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
bool get usesMaterialDesign {
|
|
return _flutterDescriptor['uses-material-design'] as bool? ?? false;
|
|
}
|
|
|
|
/// If true, does not use Swift Package Manager as a dependency manager.
|
|
/// CocoaPods will be used instead.
|
|
bool get disabledSwiftPackageManager {
|
|
return _flutterDescriptor['disable-swift-package-manager'] 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 {
|
|
final Object? module = _flutterDescriptor['module'];
|
|
if (module is YamlMap) {
|
|
return module['androidX'] == true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Any additional license files listed under the `flutter` key.
|
|
///
|
|
/// This is expected to be a list of file paths that should be treated as
|
|
/// relative to the pubspec in this directory.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```yaml
|
|
/// flutter:
|
|
/// licenses:
|
|
/// - assets/foo_license.txt
|
|
/// ```
|
|
List<String> get additionalLicenses {
|
|
final Object? licenses = _flutterDescriptor['licenses'];
|
|
if (licenses is YamlList) {
|
|
return licenses.map((Object? element) => element.toString()).toList();
|
|
}
|
|
return <String>[];
|
|
}
|
|
|
|
/// 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) {
|
|
final Object? module = _flutterDescriptor['module'];
|
|
if (module is YamlMap) {
|
|
return module['androidPackage'] as String?;
|
|
}
|
|
}
|
|
final Map<String, Object?>? platforms = supportedPlatforms;
|
|
if (platforms == null) {
|
|
// Pre-multi-platform plugin format
|
|
if (isPlugin) {
|
|
final YamlMap? plugin = _flutterDescriptor['plugin'] as YamlMap?;
|
|
return plugin?['androidPackage'] as String?;
|
|
}
|
|
return null;
|
|
}
|
|
if (platforms.containsKey('android')) {
|
|
final Object? android = platforms['android'];
|
|
if (android is YamlMap) {
|
|
return android['package'] as String?;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Returns the deferred components configuration if declared. Returns
|
|
/// null if no deferred components are declared.
|
|
late final List<DeferredComponent>? deferredComponents = computeDeferredComponents();
|
|
List<DeferredComponent>? computeDeferredComponents() {
|
|
if (!_flutterDescriptor.containsKey('deferred-components')) {
|
|
return null;
|
|
}
|
|
final List<DeferredComponent> components = <DeferredComponent>[];
|
|
final Object? deferredComponents = _flutterDescriptor['deferred-components'];
|
|
if (deferredComponents is! YamlList) {
|
|
return components;
|
|
}
|
|
for (final Object? component in deferredComponents) {
|
|
if (component is! YamlMap) {
|
|
_logger.printError('Expected deferred component manifest to be a map.');
|
|
continue;
|
|
}
|
|
components.add(
|
|
DeferredComponent(
|
|
name: component['name'] as String,
|
|
libraries: component['libraries'] == null ?
|
|
<String>[] : (component['libraries'] as List<dynamic>).cast<String>(),
|
|
assets: _computeAssets(component['assets']),
|
|
)
|
|
);
|
|
}
|
|
return components;
|
|
}
|
|
|
|
/// 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) {
|
|
final Object? module = _flutterDescriptor['module'];
|
|
if (module is YamlMap) {
|
|
return module['iosBundleIdentifier'] as String?;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Gets the supported platforms. This only supports the new `platforms` format.
|
|
///
|
|
/// If the plugin uses the legacy pubspec format, this method returns null.
|
|
Map<String, Object?>? get supportedPlatforms {
|
|
if (isPlugin) {
|
|
final YamlMap? plugin = _flutterDescriptor['plugin'] as YamlMap?;
|
|
if (plugin?.containsKey('platforms') ?? false) {
|
|
final YamlMap? platformsMap = plugin!['platforms'] as YamlMap?;
|
|
return platformsMap?.value.cast<String, Object?>();
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Like [supportedPlatforms], but only returns the valid platforms that are supported in flutter plugins.
|
|
Map<String, Object?>? get validSupportedPlatforms {
|
|
final Map<String, Object?>? allPlatforms = supportedPlatforms;
|
|
if (allPlatforms == null) {
|
|
return null;
|
|
}
|
|
final Map<String, Object?> platforms = <String, Object?>{}..addAll(allPlatforms);
|
|
platforms.removeWhere((String key, Object? _) => !_kValidPluginPlatforms.contains(key));
|
|
if (platforms.isEmpty) {
|
|
return null;
|
|
}
|
|
return platforms;
|
|
}
|
|
|
|
List<Map<String, Object?>> get fontsDescriptor {
|
|
return fonts.map((Font font) => font.descriptor).toList();
|
|
}
|
|
|
|
List<Map<String, Object?>> get _rawFontsDescriptor {
|
|
final List<Object?>? fontList = _flutterDescriptor['fonts'] as List<Object?>?;
|
|
return fontList == null
|
|
? const <Map<String, Object?>>[]
|
|
: fontList.map<Map<String, Object?>?>(castStringKeyedMap).whereType<Map<String, Object?>>().toList();
|
|
}
|
|
|
|
late final List<AssetsEntry> assets = _computeAssets(_flutterDescriptor['assets']);
|
|
|
|
late final List<Font> fonts = _extractFonts();
|
|
|
|
List<Font> _extractFonts() {
|
|
if (!_flutterDescriptor.containsKey('fonts')) {
|
|
return <Font>[];
|
|
}
|
|
|
|
final List<Font> fonts = <Font>[];
|
|
for (final Map<String, Object?> fontFamily in _rawFontsDescriptor) {
|
|
final YamlList? fontFiles = fontFamily['fonts'] as YamlList?;
|
|
final String? familyName = fontFamily['family'] as String?;
|
|
if (familyName == null) {
|
|
_logger.printWarning('Warning: Missing family name for font.', emphasis: true);
|
|
continue;
|
|
}
|
|
if (fontFiles == null) {
|
|
_logger.printWarning('Warning: No fonts specified for font $familyName', emphasis: true);
|
|
continue;
|
|
}
|
|
|
|
final List<FontAsset> fontAssets = <FontAsset>[];
|
|
for (final Map<Object?, Object?> fontFile in fontFiles.cast<Map<Object?, Object?>>()) {
|
|
final String? asset = fontFile['asset'] as String?;
|
|
if (asset == null) {
|
|
_logger.printWarning('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(familyName, fontAssets));
|
|
}
|
|
}
|
|
return fonts;
|
|
}
|
|
|
|
late final List<Uri> shaders = _extractAssetUris('shaders', 'Shader');
|
|
late final List<Uri> models = kIs3dSceneSupported ? _extractAssetUris('models', 'Model') : <Uri>[];
|
|
|
|
List<Uri> _extractAssetUris(String key, String singularName) {
|
|
if (!_flutterDescriptor.containsKey(key)) {
|
|
return <Uri>[];
|
|
}
|
|
|
|
final List<Object?>? items = _flutterDescriptor[key] as List<Object?>?;
|
|
if (items == null) {
|
|
return const <Uri>[];
|
|
}
|
|
final List<Uri> results = <Uri>[];
|
|
for (final Object? item in items) {
|
|
if (item is! String || item == '') {
|
|
_logger.printError('$singularName manifest contains a null or empty uri.');
|
|
continue;
|
|
}
|
|
try {
|
|
results.add(Uri(pathSegments: item.split('/')));
|
|
} on FormatException {
|
|
_logger.printError('$singularName manifest contains invalid uri: $item.');
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
/// Whether a synthetic flutter_gen package should be generated.
|
|
///
|
|
/// This can be provided to the [Pub] interface to inject a new entry
|
|
/// into the package_config.json file which points to `.dart_tool/flutter_gen`.
|
|
///
|
|
/// This allows generated source code to be imported using a package
|
|
/// alias.
|
|
late final bool generateSyntheticPackage = _computeGenerateSyntheticPackage();
|
|
bool _computeGenerateSyntheticPackage() {
|
|
if (!_flutterDescriptor.containsKey('generate')) {
|
|
return false;
|
|
}
|
|
final Object? value = _flutterDescriptor['generate'];
|
|
if (value is! bool) {
|
|
return false;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
String? get defaultFlavor => _flutterDescriptor['default-flavor'] as String?;
|
|
}
|
|
|
|
class Font {
|
|
Font(this.familyName, this.fontAssets)
|
|
: assert(fontAssets.isNotEmpty);
|
|
|
|
final String familyName;
|
|
final List<FontAsset> fontAssets;
|
|
|
|
Map<String, Object?> get descriptor {
|
|
return <String, Object?>{
|
|
'family': familyName,
|
|
'fonts': fontAssets.map<Map<String, Object?>>((FontAsset a) => a.descriptor).toList(),
|
|
};
|
|
}
|
|
|
|
@override
|
|
String toString() => '$runtimeType(family: $familyName, assets: $fontAssets)';
|
|
}
|
|
|
|
class FontAsset {
|
|
FontAsset(this.assetUri, {this.weight, this.style});
|
|
|
|
final Uri assetUri;
|
|
final int? weight;
|
|
final String? style;
|
|
|
|
Map<String, Object?> get descriptor {
|
|
final Map<String, Object?> descriptor = <String, Object?>{};
|
|
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)';
|
|
}
|
|
|
|
|
|
bool _validate(Object? manifest, Logger logger) {
|
|
final List<String> errors = <String>[];
|
|
if (manifest is! YamlMap) {
|
|
errors.add('Expected YAML map');
|
|
} else {
|
|
for (final MapEntry<Object?, Object?> 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}.');
|
|
}
|
|
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);
|
|
}
|
|
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) {
|
|
return;
|
|
}
|
|
for (final MapEntry<Object?, Object?> kvp in yaml.entries) {
|
|
final Object? yamlKey = kvp.key;
|
|
final Object? yamlValue = kvp.value;
|
|
if (yamlKey is! String) {
|
|
errors.add('Expected YAML key to be a string, but got $yamlKey (${yamlValue.runtimeType}).');
|
|
continue;
|
|
}
|
|
switch (yamlKey) {
|
|
case 'uses-material-design':
|
|
if (yamlValue is! bool) {
|
|
errors.add('Expected "$yamlKey" to be a bool, but got $yamlValue (${yamlValue.runtimeType}).');
|
|
}
|
|
case 'assets':
|
|
errors.addAll(_validateAssets(yamlValue));
|
|
case 'shaders':
|
|
if (yamlValue is! YamlList) {
|
|
errors.add('Expected "$yamlKey" to be a list, but got $yamlValue (${yamlValue.runtimeType}).');
|
|
} else if (yamlValue.isEmpty) {
|
|
break;
|
|
} else if (yamlValue[0] is! String) {
|
|
errors.add(
|
|
'Expected "$yamlKey" to be a list of strings, but the first element is $yamlValue (${yamlValue.runtimeType}).',
|
|
);
|
|
}
|
|
case 'models':
|
|
if (yamlValue is! YamlList) {
|
|
errors.add('Expected "$yamlKey" to be a list, but got $yamlValue (${yamlValue.runtimeType}).');
|
|
} else if (yamlValue.isEmpty) {
|
|
break;
|
|
} else if (yamlValue[0] is! String) {
|
|
errors.add(
|
|
'Expected "$yamlKey" to be a list of strings, but the first element is $yamlValue (${yamlValue.runtimeType}).',
|
|
);
|
|
}
|
|
case 'fonts':
|
|
if (yamlValue is! YamlList) {
|
|
errors.add('Expected "$yamlKey" to be a list, but got $yamlValue (${yamlValue.runtimeType}).');
|
|
} else if (yamlValue.isEmpty) {
|
|
break;
|
|
} else if (yamlValue.first is! YamlMap) {
|
|
errors.add(
|
|
'Expected "$yamlKey" to contain maps, but the first element is $yamlValue (${yamlValue.runtimeType}).',
|
|
);
|
|
} else {
|
|
_validateFonts(yamlValue, errors);
|
|
}
|
|
case 'licenses':
|
|
final (_, List<String> filesErrors) = _parseList<String>(yamlValue, '"$yamlKey"', 'files');
|
|
errors.addAll(filesErrors);
|
|
case 'module':
|
|
if (yamlValue is! YamlMap) {
|
|
errors.add('Expected "$yamlKey" to be an object, but got $yamlValue (${yamlValue.runtimeType}).');
|
|
break;
|
|
}
|
|
|
|
if (yamlValue['androidX'] != null && yamlValue['androidX'] is! bool) {
|
|
errors.add('The "androidX" value must be a bool if set.');
|
|
}
|
|
if (yamlValue['androidPackage'] != null && yamlValue['androidPackage'] is! String) {
|
|
errors.add('The "androidPackage" value must be a string if set.');
|
|
}
|
|
if (yamlValue['iosBundleIdentifier'] != null && yamlValue['iosBundleIdentifier'] is! String) {
|
|
errors.add('The "iosBundleIdentifier" section must be a string if set.');
|
|
}
|
|
case 'plugin':
|
|
if (yamlValue is! YamlMap) {
|
|
errors.add('Expected "$yamlKey" to be an object, but got $yamlValue (${yamlValue.runtimeType}).');
|
|
break;
|
|
}
|
|
final List<String> pluginErrors = Plugin.validatePluginYaml(yamlValue);
|
|
errors.addAll(pluginErrors);
|
|
case 'generate':
|
|
break;
|
|
case 'deferred-components':
|
|
_validateDeferredComponents(kvp, errors);
|
|
case 'disable-swift-package-manager':
|
|
if (yamlValue is! bool) {
|
|
errors.add('Expected "$yamlKey" to be a bool, but got $yamlValue (${yamlValue.runtimeType}).');
|
|
}
|
|
case 'default-flavor':
|
|
if (yamlValue is! String) {
|
|
errors.add('Expected "$yamlKey" to be a string, but got $yamlValue (${yamlValue.runtimeType}).');
|
|
}
|
|
default:
|
|
errors.add('Unexpected child "$yamlKey" found under "flutter".');
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
(List<T>? result, List<String> errors) _parseList<T>(Object? yamlList, String context, String typeAlias) {
|
|
final List<String> errors = <String>[];
|
|
|
|
if (yamlList is! YamlList) {
|
|
final String message = 'Expected $context to be a list of $typeAlias, but got $yamlList (${yamlList.runtimeType}).';
|
|
return (null, <String>[message]);
|
|
}
|
|
|
|
for (int i = 0; i < yamlList.length; i++) {
|
|
if (yamlList[i] is! T) {
|
|
// ignore: avoid_dynamic_calls
|
|
errors.add('Expected $context to be a list of $typeAlias, but element at index $i was a ${yamlList[i].runtimeType}.');
|
|
}
|
|
}
|
|
|
|
return errors.isEmpty ? (List<T>.from(yamlList), errors) : (null, errors);
|
|
}
|
|
|
|
void _validateDeferredComponents(MapEntry<Object?, Object?> kvp, List<String> errors) {
|
|
final Object? yamlList = kvp.value;
|
|
if (yamlList != null && (yamlList is! YamlList || yamlList[0] is! YamlMap)) {
|
|
errors.add('Expected "${kvp.key}" to be a list, but got $yamlList (${yamlList.runtimeType}).');
|
|
} else if (yamlList is YamlList) {
|
|
for (int i = 0; i < yamlList.length; i++) {
|
|
final Object? valueMap = yamlList[i];
|
|
if (valueMap is! YamlMap) {
|
|
// ignore: avoid_dynamic_calls
|
|
errors.add('Expected the $i element in "${kvp.key}" to be a map, but got ${yamlList[i]} (${yamlList[i].runtimeType}).');
|
|
continue;
|
|
}
|
|
if (!valueMap.containsKey('name') || valueMap['name'] is! String) {
|
|
errors.add('Expected the $i element in "${kvp.key}" to have required key "name" of type String');
|
|
}
|
|
if (valueMap.containsKey('libraries')) {
|
|
final (_, List<String> librariesErrors) = _parseList<String>(
|
|
valueMap['libraries'],
|
|
'"libraries" key in the element at index $i of "${kvp.key}"',
|
|
'String',
|
|
);
|
|
errors.addAll(librariesErrors);
|
|
}
|
|
if (valueMap.containsKey('assets')) {
|
|
errors.addAll(_validateAssets(valueMap['assets']));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
List<String> _validateAssets(Object? yaml) {
|
|
final (_, List<String> errors) = _computeAssetsSafe(yaml);
|
|
return errors;
|
|
}
|
|
|
|
// TODO(andrewkolos): We end up parsing the assets section twice, once during
|
|
// validation and once when the assets getter is called. We should consider
|
|
// refactoring this class to parse and store everything in the constructor.
|
|
// https://github.com/flutter/flutter/issues/139183
|
|
(List<AssetsEntry>, List<String> errors) _computeAssetsSafe(Object? yaml) {
|
|
if (yaml == null) {
|
|
return (const <AssetsEntry>[], const <String>[]);
|
|
}
|
|
if (yaml is! YamlList) {
|
|
final String error = 'Expected "assets" to be a list, but got $yaml (${yaml.runtimeType}).';
|
|
return (const <AssetsEntry>[], <String>[error]);
|
|
}
|
|
final List<AssetsEntry> results = <AssetsEntry>[];
|
|
final List<String> errors = <String>[];
|
|
for (final Object? rawAssetEntry in yaml) {
|
|
final (AssetsEntry? parsed, String? error) = AssetsEntry.parseFromYamlSafe(rawAssetEntry);
|
|
if (parsed != null) {
|
|
results.add(parsed);
|
|
}
|
|
if (error != null) {
|
|
errors.add(error);
|
|
}
|
|
}
|
|
return (results, errors);
|
|
}
|
|
|
|
List<AssetsEntry> _computeAssets(Object? assetsSection) {
|
|
final (List<AssetsEntry> result, List<String> errors) = _computeAssetsSafe(assetsSection);
|
|
if (errors.isNotEmpty) {
|
|
throw Exception('Uncaught error(s) in assets section: '
|
|
'${errors.join('\n')}');
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void _validateFonts(YamlList fonts, List<String> errors) {
|
|
const Set<int> fontWeights = <int>{
|
|
100, 200, 300, 400, 500, 600, 700, 800, 900,
|
|
};
|
|
for (final Object? fontMap in fonts) {
|
|
if (fontMap is! YamlMap) {
|
|
errors.add('Unexpected child "$fontMap" found under "fonts". Expected a map.');
|
|
continue;
|
|
}
|
|
for (final Object? key in fontMap.keys.where((Object? 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 Object? fontMapList in fontMap['fonts'] as List<Object?>) {
|
|
if (fontMapList is! YamlMap) {
|
|
errors.add('Expected "fonts" to be a list of maps.');
|
|
continue;
|
|
}
|
|
for (final MapEntry<Object?, Object?> kvp in fontMapList.entries) {
|
|
final Object? fontKey = kvp.key;
|
|
if (fontKey is! String) {
|
|
errors.add('Expected "$fontKey" under "fonts" to be a string.');
|
|
}
|
|
switch (fontKey) {
|
|
case 'asset':
|
|
if (kvp.value is! String) {
|
|
errors.add('Expected font asset ${kvp.value} ((${kvp.value.runtimeType})) to be a string.');
|
|
}
|
|
case 'weight':
|
|
if (!fontWeights.contains(kvp.value)) {
|
|
errors.add('Invalid value ${kvp.value} ((${kvp.value.runtimeType})) for font -> weight.');
|
|
}
|
|
case 'style':
|
|
if (kvp.value != 'normal' && kvp.value != 'italic') {
|
|
errors.add('Invalid value ${kvp.value} ((${kvp.value.runtimeType})) for font -> style.');
|
|
}
|
|
default:
|
|
errors.add('Unexpected key $fontKey ((${kvp.value.runtimeType})) under font.');
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Represents an entry under the `assets` section of a pubspec.
|
|
@immutable
|
|
class AssetsEntry {
|
|
const AssetsEntry({
|
|
required this.uri,
|
|
this.flavors = const <String>{},
|
|
this.transformers = const <AssetTransformerEntry>[],
|
|
});
|
|
|
|
final Uri uri;
|
|
final Set<String> flavors;
|
|
final List<AssetTransformerEntry> transformers;
|
|
|
|
static const String _pathKey = 'path';
|
|
static const String _flavorKey = 'flavors';
|
|
static const String _transformersKey = 'transformers';
|
|
|
|
static AssetsEntry? parseFromYaml(Object? yaml) {
|
|
final (AssetsEntry? value, String? error) = parseFromYamlSafe(yaml);
|
|
if (error != null) {
|
|
throw Exception('Unexpected error when parsing assets entry');
|
|
}
|
|
return value!;
|
|
}
|
|
|
|
static (AssetsEntry? assetsEntry, String? error) parseFromYamlSafe(Object? yaml) {
|
|
|
|
(Uri?, String?) tryParseUri(String uri) {
|
|
try {
|
|
return (Uri(pathSegments: uri.split('/')), null);
|
|
} on FormatException {
|
|
return (null, 'Asset manifest contains invalid uri: $uri.');
|
|
}
|
|
}
|
|
|
|
if (yaml == null || yaml == '') {
|
|
return (null, 'Asset manifest contains a null or empty uri.');
|
|
}
|
|
|
|
if (yaml is String) {
|
|
final (Uri? uri, String? error) = tryParseUri(yaml);
|
|
return uri == null ? (null, error) : (AssetsEntry(uri: uri), null);
|
|
}
|
|
|
|
if (yaml is Map) {
|
|
if (yaml.keys.isEmpty) {
|
|
return (null, null);
|
|
}
|
|
|
|
final Object? path = yaml[_pathKey];
|
|
|
|
if (path == null || path is! String) {
|
|
return (null, 'Asset manifest entry is malformed. '
|
|
'Expected asset entry to be either a string or a map '
|
|
'containing a "$_pathKey" entry. Got ${path.runtimeType} instead.');
|
|
}
|
|
|
|
final (List<String>? flavors, List<String> flavorsErrors) = _parseFlavorsSection(yaml[_flavorKey]);
|
|
final (List<AssetTransformerEntry>? transformers, List<String> transformersErrors) = _parseTransformersSection(yaml[_transformersKey]);
|
|
|
|
final List<String> errors = <String>[
|
|
...flavorsErrors.map((String e) => 'In $_flavorKey section of asset "$path": $e'),
|
|
...transformersErrors.map((String e) => 'In $_transformersKey section of asset "$path": $e'),
|
|
];
|
|
if (errors.isNotEmpty) {
|
|
return (
|
|
null,
|
|
<String>[
|
|
'Unable to parse assets section.',
|
|
...errors
|
|
].join('\n'),
|
|
);
|
|
}
|
|
|
|
return (
|
|
AssetsEntry(
|
|
uri: Uri(pathSegments: path.split('/')),
|
|
flavors: Set<String>.from(flavors ?? <String>[]),
|
|
transformers: transformers ?? <AssetTransformerEntry>[],
|
|
),
|
|
null,
|
|
);
|
|
}
|
|
|
|
return (null, 'Assets entry had unexpected shape. '
|
|
'Expected a string or an object. Got ${yaml.runtimeType} instead.');
|
|
}
|
|
|
|
static (List<String>? flavors, List<String> errors) _parseFlavorsSection(Object? yaml) {
|
|
if (yaml == null) {
|
|
return (null, <String>[]);
|
|
}
|
|
|
|
return _parseList<String>(yaml, _flavorKey, 'String');
|
|
}
|
|
|
|
static (List<AssetTransformerEntry>?, List<String> errors) _parseTransformersSection(Object? yaml) {
|
|
if (yaml == null) {
|
|
return (null, <String>[]);
|
|
}
|
|
final (List<YamlMap>? yamlObjects, List<String> listErrors) = _parseList<YamlMap>(
|
|
yaml,
|
|
'$_transformersKey list',
|
|
'Map',
|
|
);
|
|
|
|
if (listErrors.isNotEmpty) {
|
|
return (null, listErrors);
|
|
}
|
|
|
|
final List<AssetTransformerEntry> transformers = <AssetTransformerEntry>[];
|
|
final List<String> errors = <String>[];
|
|
for (final YamlMap yaml in yamlObjects!) {
|
|
final (AssetTransformerEntry? transformerEntry, List<String> transformerErrors) = AssetTransformerEntry.tryParse(yaml);
|
|
if (transformerEntry != null) {
|
|
transformers.add(transformerEntry);
|
|
} else {
|
|
errors.addAll(transformerErrors);
|
|
}
|
|
}
|
|
|
|
if (errors.isEmpty) {
|
|
return (transformers, errors);
|
|
}
|
|
return (null, errors);
|
|
}
|
|
|
|
@override
|
|
bool operator ==(Object other) {
|
|
if (other is! AssetsEntry) {
|
|
return false;
|
|
}
|
|
|
|
return uri == other.uri && setEquals(flavors, other.flavors);
|
|
}
|
|
|
|
@override
|
|
int get hashCode => Object.hashAll(<Object?>[
|
|
uri.hashCode,
|
|
Object.hashAllUnordered(flavors),
|
|
Object.hashAll(transformers),
|
|
]);
|
|
|
|
@override
|
|
String toString() => 'AssetsEntry(uri: $uri, flavors: $flavors, transformers: $transformers)';
|
|
}
|
|
|
|
|
|
/// Represents an entry in the "transformers" section of an asset.
|
|
@immutable
|
|
final class AssetTransformerEntry {
|
|
const AssetTransformerEntry({
|
|
required this.package,
|
|
required List<String>? args,
|
|
}): args = args ?? const <String>[];
|
|
|
|
final String package;
|
|
final List<String>? args;
|
|
|
|
static (AssetTransformerEntry? entry, List<String> errors) tryParse(Object? yaml) {
|
|
if (yaml == null) {
|
|
return (null, <String>['Transformer entry is null.']);
|
|
}
|
|
if (yaml is! YamlMap) {
|
|
return (null, <String>['Expected entry to be a map. Found ${yaml.runtimeType} instead']);
|
|
}
|
|
|
|
final Object? package = yaml['package'];
|
|
if (package is! String || package.isEmpty) {
|
|
return (null, <String>['Expected "package" to be a String. Found ${package.runtimeType} instead.']);
|
|
}
|
|
|
|
final (List<String>? args, List<String> argsErrors) = _parseArgsSection(yaml['args']);
|
|
if (argsErrors.isNotEmpty) {
|
|
return (null, argsErrors.map((String e) => 'In args section of transformer using package "$package": $e').toList());
|
|
}
|
|
|
|
return (
|
|
AssetTransformerEntry(
|
|
package: package,
|
|
args: args,
|
|
),
|
|
<String>[],
|
|
);
|
|
}
|
|
|
|
static (List<String>? args, List<String> errors) _parseArgsSection(Object? yaml) {
|
|
if (yaml == null) {
|
|
return (null, <String>[]);
|
|
}
|
|
return _parseList(yaml, 'args', 'String');
|
|
}
|
|
|
|
@override
|
|
bool operator ==(Object other) {
|
|
if (identical(this, other)) {
|
|
return true;
|
|
}
|
|
if (other is! AssetTransformerEntry) {
|
|
return false;
|
|
}
|
|
|
|
final bool argsAreEqual = (() {
|
|
if (args == null && other.args == null) {
|
|
return true;
|
|
}
|
|
if (args?.length != other.args?.length) {
|
|
return false;
|
|
}
|
|
|
|
for (int index = 0; index < args!.length; index += 1) {
|
|
if (args![index] != other.args![index]) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
})();
|
|
|
|
return package == other.package && argsAreEqual;
|
|
}
|
|
|
|
@override
|
|
int get hashCode => Object.hashAll(
|
|
<Object?>[
|
|
package.hashCode,
|
|
args?.map((String e) => e.hashCode),
|
|
],
|
|
);
|
|
|
|
@override
|
|
String toString() {
|
|
return 'AssetTransformerEntry(package: $package, args: $args)';
|
|
}
|
|
}
|