1// Copyright 2014 The Flutter 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
5/// @docImport 'localizations/gen_l10n.dart';
6library;
7
8import 'package:meta/meta.dart';
9import 'package:pub_semver/pub_semver.dart';
10import 'package:yaml/yaml.dart';
11
12import 'base/deferred_component.dart';
13import 'base/file_system.dart';
14import 'base/logger.dart';
15import 'base/utils.dart';
16import 'globals.dart' as globals;
17import 'plugins.dart';
18
19const Set<String> _kValidPluginPlatforms = <String>{
20 'android',
21 'ios',
22 'web',
23 'windows',
24 'linux',
25 'macos',
26};
27
28/// A wrapper around the `flutter` section in the `pubspec.yaml` file.
29class FlutterManifest {
30 FlutterManifest._({required Logger logger}) : _logger = logger;
31
32 /// Returns an empty manifest.
33 factory FlutterManifest.empty({required Logger logger}) = FlutterManifest._;
34
35 /// Returns null on invalid manifest. Returns empty manifest on missing file.
36 static FlutterManifest? createFromPath(
37 String path, {
38 required FileSystem fileSystem,
39 required Logger logger,
40 }) {
41 if (!fileSystem.isFileSync(path)) {
42 return _createFromYaml(null, logger);
43 }
44 final String manifest = fileSystem.file(path).readAsStringSync();
45 return FlutterManifest.createFromString(manifest, logger: logger);
46 }
47
48 /// Returns null on missing or invalid manifest.
49 @visibleForTesting
50 static FlutterManifest? createFromString(String manifest, {required Logger logger}) {
51 return _createFromYaml(loadYaml(manifest), logger);
52 }
53
54 static FlutterManifest? _createFromYaml(Object? yamlDocument, Logger logger) {
55 if (yamlDocument != null && !_validate(yamlDocument, logger)) {
56 return null;
57 }
58
59 final FlutterManifest pubspec = FlutterManifest._(logger: logger);
60 final Map<Object?, Object?>? yamlMap = yamlDocument as YamlMap?;
61 if (yamlMap != null) {
62 pubspec._descriptor = yamlMap.cast<String, Object?>();
63 }
64
65 final Map<Object?, Object?>? flutterMap =
66 pubspec._descriptor['flutter'] as Map<Object?, Object?>?;
67 if (flutterMap != null) {
68 pubspec._flutterDescriptor = flutterMap.cast<String, Object?>();
69 }
70
71 return pubspec;
72 }
73
74 /// Creates a copy of the current manifest with some subset of properties
75 /// modified.
76 FlutterManifest copyWith({
77 required Logger logger,
78 List<AssetsEntry>? assets,
79 List<Font>? fonts,
80 List<Uri>? shaders,
81 List<DeferredComponent>? deferredComponents,
82 }) {
83 final FlutterManifest copy = FlutterManifest._(logger: _logger);
84 copy._descriptor = <String, Object?>{..._descriptor};
85 copy._flutterDescriptor = <String, Object?>{..._flutterDescriptor};
86
87 if (assets != null && assets.isNotEmpty) {
88 copy._flutterDescriptor['assets'] = YamlList.wrap(<Object?>[
89 for (final AssetsEntry asset in assets) asset.descriptor,
90 ]);
91 }
92
93 if (fonts != null && fonts.isNotEmpty) {
94 copy._flutterDescriptor['fonts'] = YamlList.wrap(<Map<String, Object?>>[
95 for (final Font font in fonts) font.descriptor,
96 ]);
97 }
98
99 if (shaders != null && shaders.isNotEmpty) {
100 copy._flutterDescriptor['shaders'] = YamlList.wrap(
101 shaders.map((Uri uri) => uri.toString()).toList(),
102 );
103 }
104
105 if (deferredComponents != null && deferredComponents.isNotEmpty) {
106 copy._flutterDescriptor['deferred-components'] = YamlList.wrap(
107 deferredComponents.map((DeferredComponent dc) => dc.descriptor).toList(),
108 );
109 }
110
111 copy._descriptor['flutter'] = YamlMap.wrap(copy._flutterDescriptor);
112
113 if (!_validate(YamlMap.wrap(copy._descriptor), logger)) {
114 throw StateError('Generated invalid pubspec.yaml.');
115 }
116
117 return copy;
118 }
119
120 final Logger _logger;
121
122 /// A map representation of the entire `pubspec.yaml` file.
123 Map<String, Object?> _descriptor = <String, Object?>{};
124
125 /// A map representation of the `flutter` section in the `pubspec.yaml` file.
126 Map<String, Object?> _flutterDescriptor = <String, Object?>{};
127
128 Map<String, Object?> get flutterDescriptor => _flutterDescriptor;
129
130 /// True if the `pubspec.yaml` file does not exist.
131 bool get isEmpty => _descriptor.isEmpty;
132
133 /// The string value of the top-level `name` property in the `pubspec.yaml` file.
134 String get appName => _descriptor['name'] as String? ?? '';
135
136 /// Contains the name of the dependencies.
137 /// These are the keys specified in the `dependency` map.
138 Set<String> get dependencies {
139 final YamlMap? dependencies = _descriptor['dependencies'] as YamlMap?;
140 return dependencies != null ? <String>{...dependencies.keys.cast<String>()} : <String>{};
141 }
142
143 /// List of all the entries in the workspace field of the `pubspec.yaml` file.
144 List<String> get workspace =>
145 (_descriptor['workspace'] as YamlList?)?.cast<String>() ?? <String>[];
146
147 // Flag to avoid printing multiple invalid version messages.
148 bool _hasShowInvalidVersionMsg = false;
149
150 /// The version String from the `pubspec.yaml` file.
151 /// Can be null if it isn't set or has a wrong format.
152 String? get appVersion {
153 final String? verStr = _descriptor['version']?.toString();
154 if (verStr == null) {
155 return null;
156 }
157
158 Version? version;
159 try {
160 version = Version.parse(verStr);
161 } on Exception {
162 if (!_hasShowInvalidVersionMsg) {
163 _logger.printStatus(
164 globals.userMessages.invalidVersionSettingHintMessage(verStr),
165 emphasis: true,
166 );
167 _hasShowInvalidVersionMsg = true;
168 }
169 }
170 return version?.toString();
171 }
172
173 /// The build version name from the `pubspec.yaml` file.
174 /// Can be null if version isn't set or has a wrong format.
175 String? get buildName {
176 final String? version = appVersion;
177 if (version != null && version.contains('+')) {
178 return version.split('+').elementAt(0);
179 }
180 return version;
181 }
182
183 /// The build version number from the `pubspec.yaml` file.
184 /// Can be null if version isn't set or has a wrong format.
185 String? get buildNumber {
186 final String? version = appVersion;
187 if (version != null && version.contains('+')) {
188 final String value = version.split('+').elementAt(1);
189 return value;
190 } else {
191 return null;
192 }
193 }
194
195 bool get usesMaterialDesign {
196 return _flutterDescriptor['uses-material-design'] as bool? ?? false;
197 }
198
199 /// If true, does not use Swift Package Manager as a dependency manager.
200 /// CocoaPods will be used instead.
201 bool get disabledSwiftPackageManager {
202 return _flutterDescriptor['disable-swift-package-manager'] as bool? ?? false;
203 }
204
205 /// True if this Flutter module should use AndroidX dependencies.
206 ///
207 /// If false the deprecated Android Support library will be used.
208 bool get usesAndroidX {
209 final Object? module = _flutterDescriptor['module'];
210 if (module is YamlMap) {
211 return module['androidX'] == true;
212 }
213 return false;
214 }
215
216 /// Any additional license files listed under the `flutter` key.
217 ///
218 /// This is expected to be a list of file paths that should be treated as
219 /// relative to the pubspec in this directory.
220 ///
221 /// For example:
222 ///
223 /// ```yaml
224 /// flutter:
225 /// licenses:
226 /// - assets/foo_license.txt
227 /// ```
228 List<String> get additionalLicenses {
229 return <String>[
230 if (_flutterDescriptor case {'licenses': final YamlList list})
231 for (final Object? item in list) '$item',
232 ];
233 }
234
235 /// True if this manifest declares a Flutter module project.
236 ///
237 /// A Flutter project is considered a module when it has a `module:`
238 /// descriptor. A Flutter module project supports integration into an
239 /// existing host app, and has managed platform host code.
240 ///
241 /// Such a project can be created using `flutter create -t module`.
242 bool get isModule => _flutterDescriptor.containsKey('module');
243
244 /// True if this manifest declares a Flutter plugin project.
245 ///
246 /// A Flutter project is considered a plugin when it has a `plugin:`
247 /// descriptor. A Flutter plugin project wraps custom Android and/or
248 /// iOS code in a Dart interface for consumption by other Flutter app
249 /// projects.
250 ///
251 /// Such a project can be created using `flutter create -t plugin`.
252 bool get isPlugin => _flutterDescriptor.containsKey('plugin');
253
254 /// Returns the Android package declared by this manifest in its
255 /// module or plugin descriptor. Returns null, if there is no
256 /// such declaration.
257 String? get androidPackage {
258 if (isModule) {
259 if (_flutterDescriptor case {'module': final YamlMap map}) {
260 return map['androidPackage'] as String?;
261 }
262 }
263
264 late final YamlMap? plugin = _flutterDescriptor['plugin'] as YamlMap?;
265
266 return switch (supportedPlatforms) {
267 {'android': final YamlMap map} => map['package'] as String?,
268 // Pre-multi-platform plugin format
269 null when isPlugin => plugin?['androidPackage'] as String?,
270 _ => null,
271 };
272 }
273
274 /// Returns the deferred components configuration if declared. Returns
275 /// null if no deferred components are declared.
276 late final List<DeferredComponent>? deferredComponents = computeDeferredComponents();
277 List<DeferredComponent>? computeDeferredComponents() {
278 if (!_flutterDescriptor.containsKey('deferred-components')) {
279 return null;
280 }
281 final List<DeferredComponent> components = <DeferredComponent>[];
282 final Object? deferredComponents = _flutterDescriptor['deferred-components'];
283 if (deferredComponents is! YamlList) {
284 return components;
285 }
286 for (final Object? component in deferredComponents) {
287 if (component is! YamlMap) {
288 _logger.printError('Expected deferred component manifest to be a map.');
289 continue;
290 }
291 components.add(
292 DeferredComponent(
293 name: component['name'] as String,
294 libraries:
295 component['libraries'] == null
296 ? <String>[]
297 : (component['libraries'] as List<dynamic>).cast<String>(),
298 assets: _computeAssets(component['assets']),
299 ),
300 );
301 }
302 return components;
303 }
304
305 /// Returns the iOS bundle identifier declared by this manifest in its
306 /// module descriptor. Returns null if there is no such declaration.
307 String? get iosBundleIdentifier {
308 if (isModule) {
309 if (_flutterDescriptor case {'module': final YamlMap map}) {
310 return map['iosBundleIdentifier'] as String?;
311 }
312 }
313 return null;
314 }
315
316 /// Gets the supported platforms. This only supports the new `platforms` format.
317 ///
318 /// If the plugin uses the legacy pubspec format, this method returns null.
319 Map<String, Object?>? get supportedPlatforms {
320 if (isPlugin) {
321 final YamlMap? plugin = _flutterDescriptor['plugin'] as YamlMap?;
322 if (plugin?.containsKey('platforms') ?? false) {
323 final YamlMap? platformsMap = plugin!['platforms'] as YamlMap?;
324 return platformsMap?.value.cast<String, Object?>();
325 }
326 }
327 return null;
328 }
329
330 /// Like [supportedPlatforms], but only returns the valid platforms that are supported in flutter plugins.
331 Map<String, Object?>? get validSupportedPlatforms {
332 final Map<String, Object?>? allPlatforms = supportedPlatforms;
333 if (allPlatforms == null) {
334 return null;
335 }
336 final Map<String, Object?> platforms = <String, Object?>{}..addAll(allPlatforms);
337 platforms.removeWhere((String key, Object? _) => !_kValidPluginPlatforms.contains(key));
338 if (platforms.isEmpty) {
339 return null;
340 }
341 return platforms;
342 }
343
344 List<Map<String, Object?>> get fontsDescriptor {
345 return fonts.map((Font font) => font.descriptor).toList();
346 }
347
348 List<Map<String, Object?>> get _rawFontsDescriptor {
349 final List<Object?>? fontList = _flutterDescriptor['fonts'] as List<Object?>?;
350 return fontList == null
351 ? const <Map<String, Object?>>[]
352 : fontList
353 .map<Map<String, Object?>?>(castStringKeyedMap)
354 .whereType<Map<String, Object?>>()
355 .toList();
356 }
357
358 late final List<AssetsEntry> assets = _computeAssets(_flutterDescriptor['assets']);
359
360 late final List<Font> fonts = _extractFonts();
361
362 List<Font> _extractFonts() {
363 if (!_flutterDescriptor.containsKey('fonts')) {
364 return <Font>[];
365 }
366
367 final List<Font> fonts = <Font>[];
368 for (final Map<String, Object?> fontFamily in _rawFontsDescriptor) {
369 final YamlList? fontFiles = fontFamily['fonts'] as YamlList?;
370 final String? familyName = fontFamily['family'] as String?;
371 if (familyName == null) {
372 _logger.printWarning('Warning: Missing family name for font.', emphasis: true);
373 continue;
374 }
375 if (fontFiles == null) {
376 _logger.printWarning('Warning: No fonts specified for font $familyName', emphasis: true);
377 continue;
378 }
379
380 final List<FontAsset> fontAssets = <FontAsset>[];
381 for (final Map<Object?, Object?> fontFile in fontFiles.cast<Map<Object?, Object?>>()) {
382 final String? asset = fontFile['asset'] as String?;
383 if (asset == null) {
384 _logger.printWarning('Warning: Missing asset in fonts for $familyName', emphasis: true);
385 continue;
386 }
387
388 fontAssets.add(
389 FontAsset(
390 Uri.parse(asset),
391 weight: fontFile['weight'] as int?,
392 style: fontFile['style'] as String?,
393 ),
394 );
395 }
396 if (fontAssets.isNotEmpty) {
397 fonts.add(Font(familyName, fontAssets));
398 }
399 }
400 return fonts;
401 }
402
403 late final List<Uri> shaders = _extractAssetUris('shaders', 'Shader');
404
405 List<Uri> _extractAssetUris(String key, String singularName) {
406 if (!_flutterDescriptor.containsKey(key)) {
407 return <Uri>[];
408 }
409
410 final List<Object?>? items = _flutterDescriptor[key] as List<Object?>?;
411 if (items == null) {
412 return const <Uri>[];
413 }
414 final List<Uri> results = <Uri>[];
415 for (final Object? item in items) {
416 if (item is! String || item == '') {
417 _logger.printError('$singularName manifest contains a null or empty uri.');
418 continue;
419 }
420 try {
421 results.add(Uri(pathSegments: item.split('/')));
422 } on FormatException {
423 _logger.printError('$singularName manifest contains invalid uri: $item.');
424 }
425 }
426 return results;
427 }
428
429 /// Whether localization Dart files should be generated.
430 ///
431 /// **NOTE**: This method was previously called `generateSyntheticPackage`,
432 /// which was incorrect; the presence of `generate: true` in `pubspec.yaml`
433 /// does _not_ imply a synthetic package (and never did); additional
434 /// introspection is required to determine whether a synthetic package is
435 /// required.
436 ///
437 /// See also:
438 ///
439 /// * [Deprecate and remove synthethic `package:flutter_gen`](https://github.com/flutter/flutter/issues/102983)
440 late final bool generateLocalizations = _flutterDescriptor['generate'] == true;
441
442 String? get defaultFlavor => _flutterDescriptor['default-flavor'] as String?;
443
444 YamlMap toYaml() {
445 return YamlMap.wrap(_descriptor);
446 }
447}
448
449class Font {
450 Font(this.familyName, this.fontAssets) : assert(fontAssets.isNotEmpty);
451
452 final String familyName;
453 final List<FontAsset> fontAssets;
454
455 Map<String, Object?> get descriptor {
456 return <String, Object?>{
457 'family': familyName,
458 'fonts': fontAssets.map<Map<String, Object?>>((FontAsset a) => a.descriptor).toList(),
459 };
460 }
461
462 @override
463 String toString() => '$runtimeType(family: $familyName, assets: $fontAssets)';
464}
465
466class FontAsset {
467 FontAsset(this.assetUri, {this.weight, this.style});
468
469 final Uri assetUri;
470 final int? weight;
471 final String? style;
472
473 Map<String, Object?> get descriptor {
474 final Map<String, Object?> descriptor = <String, Object?>{};
475 if (weight != null) {
476 descriptor['weight'] = weight;
477 }
478
479 if (style != null) {
480 descriptor['style'] = style;
481 }
482
483 descriptor['asset'] = assetUri.path;
484 return descriptor;
485 }
486
487 @override
488 String toString() => '$runtimeType(asset: ${assetUri.path}, weight; $weight, style: $style)';
489}
490
491bool _validate(Object? manifest, Logger logger) {
492 final List<String> errors = <String>[];
493 if (manifest is! YamlMap) {
494 errors.add('Expected YAML map');
495 } else {
496 for (final MapEntry<Object?, Object?> kvp in manifest.entries) {
497 if (kvp.key is! String) {
498 errors.add('Expected YAML key to be a string, but got ${kvp.key}.');
499 continue;
500 }
501 switch (kvp.key as String?) {
502 case 'name':
503 if (kvp.value is! String) {
504 errors.add('Expected "${kvp.key}" to be a string, but got ${kvp.value}.');
505 }
506 case 'flutter':
507 if (kvp.value == null) {
508 continue;
509 }
510 if (kvp.value is! YamlMap) {
511 errors.add(
512 'Expected "${kvp.key}" section to be an object or null, but got ${kvp.value}.',
513 );
514 } else {
515 _validateFlutter(kvp.value as YamlMap?, errors);
516 }
517 default:
518 // additionalProperties are allowed.
519 break;
520 }
521 }
522 }
523
524 if (errors.isNotEmpty) {
525 logger.printStatus('Error detected in pubspec.yaml:', emphasis: true);
526 logger.printError(errors.join('\n'));
527 return false;
528 }
529
530 return true;
531}
532
533void _validateFlutter(YamlMap? yaml, List<String> errors) {
534 if (yaml == null) {
535 return;
536 }
537 for (final MapEntry<Object?, Object?> kvp in yaml.entries) {
538 final Object? yamlKey = kvp.key;
539 final Object? yamlValue = kvp.value;
540 if (yamlKey is! String) {
541 errors.add('Expected YAML key to be a string, but got $yamlKey (${yamlValue.runtimeType}).');
542 continue;
543 }
544 switch (yamlKey) {
545 case 'uses-material-design':
546 if (yamlValue is! bool) {
547 errors.add(
548 'Expected "$yamlKey" to be a bool, but got $yamlValue (${yamlValue.runtimeType}).',
549 );
550 }
551 case 'assets':
552 errors.addAll(_validateAssets(yamlValue));
553 case 'shaders':
554 if (yamlValue is! YamlList) {
555 errors.add(
556 'Expected "$yamlKey" to be a list, but got $yamlValue (${yamlValue.runtimeType}).',
557 );
558 } else if (yamlValue.isEmpty) {
559 break;
560 } else if (yamlValue[0] is! String) {
561 errors.add(
562 'Expected "$yamlKey" to be a list of strings, but the first element is $yamlValue (${yamlValue.runtimeType}).',
563 );
564 }
565 case 'fonts':
566 if (yamlValue is! YamlList) {
567 errors.add(
568 'Expected "$yamlKey" to be a list, but got $yamlValue (${yamlValue.runtimeType}).',
569 );
570 } else if (yamlValue.isEmpty) {
571 break;
572 } else if (yamlValue.first is! YamlMap) {
573 errors.add(
574 'Expected "$yamlKey" to contain maps, but the first element is $yamlValue (${yamlValue.runtimeType}).',
575 );
576 } else {
577 _validateFonts(yamlValue, errors);
578 }
579 case 'licenses':
580 final (_, List<String> filesErrors) = _parseList<String>(yamlValue, '"$yamlKey"', 'files');
581 errors.addAll(filesErrors);
582 case 'module':
583 if (yamlValue is! YamlMap) {
584 errors.add(
585 'Expected "$yamlKey" to be an object, but got $yamlValue (${yamlValue.runtimeType}).',
586 );
587 break;
588 }
589
590 if (yamlValue['androidX'] != null && yamlValue['androidX'] is! bool) {
591 errors.add('The "androidX" value must be a bool if set.');
592 }
593 if (yamlValue['androidPackage'] != null && yamlValue['androidPackage'] is! String) {
594 errors.add('The "androidPackage" value must be a string if set.');
595 }
596 if (yamlValue['iosBundleIdentifier'] != null &&
597 yamlValue['iosBundleIdentifier'] is! String) {
598 errors.add('The "iosBundleIdentifier" section must be a string if set.');
599 }
600 case 'plugin':
601 if (yamlValue is! YamlMap) {
602 errors.add(
603 'Expected "$yamlKey" to be an object, but got $yamlValue (${yamlValue.runtimeType}).',
604 );
605 break;
606 }
607 final List<String> pluginErrors = Plugin.validatePluginYaml(yamlValue);
608 errors.addAll(pluginErrors);
609 case 'generate':
610 break;
611 case 'deferred-components':
612 _validateDeferredComponents(kvp, errors);
613 case 'disable-swift-package-manager':
614 if (yamlValue is! bool) {
615 errors.add(
616 'Expected "$yamlKey" to be a bool, but got $yamlValue (${yamlValue.runtimeType}).',
617 );
618 }
619 case 'default-flavor':
620 if (yamlValue is! String) {
621 errors.add(
622 'Expected "$yamlKey" to be a string, but got $yamlValue (${yamlValue.runtimeType}).',
623 );
624 }
625 default:
626 errors.add('Unexpected child "$yamlKey" found under "flutter".');
627 }
628 }
629}
630
631(List<T>? result, List<String> errors) _parseList<T>(
632 Object? yamlList,
633 String context,
634 String typeAlias,
635) {
636 final List<String> errors = <String>[];
637
638 if (yamlList is! YamlList) {
639 final String message =
640 'Expected $context to be a list of $typeAlias, but got $yamlList (${yamlList.runtimeType}).';
641 return (null, <String>[message]);
642 }
643
644 for (int i = 0; i < yamlList.length; i++) {
645 if (yamlList[i] is! T) {
646 errors.add(
647 'Expected $context to be a list of $typeAlias, but element at index $i was a ${yamlList[i].runtimeType}.',
648 );
649 }
650 }
651
652 return errors.isEmpty ? (List<T>.from(yamlList), errors) : (null, errors);
653}
654
655void _validateDeferredComponents(MapEntry<Object?, Object?> kvp, List<String> errors) {
656 final Object? yamlList = kvp.value;
657 if (yamlList != null && (yamlList is! YamlList || yamlList[0] is! YamlMap)) {
658 errors.add('Expected "${kvp.key}" to be a list, but got $yamlList (${yamlList.runtimeType}).');
659 } else if (yamlList is YamlList) {
660 for (int i = 0; i < yamlList.length; i++) {
661 final Object? valueMap = yamlList[i];
662 if (valueMap is! YamlMap) {
663 errors.add(
664 'Expected the $i element in "${kvp.key}" to be a map, but got ${yamlList[i]} (${yamlList[i].runtimeType}).',
665 );
666 continue;
667 }
668 if (!valueMap.containsKey('name') || valueMap['name'] is! String) {
669 errors.add(
670 'Expected the $i element in "${kvp.key}" to have required key "name" of type String',
671 );
672 }
673 if (valueMap.containsKey('libraries')) {
674 final (_, List<String> librariesErrors) = _parseList<String>(
675 valueMap['libraries'],
676 '"libraries" key in the element at index $i of "${kvp.key}"',
677 'String',
678 );
679 errors.addAll(librariesErrors);
680 }
681 if (valueMap.containsKey('assets')) {
682 errors.addAll(_validateAssets(valueMap['assets']));
683 }
684 }
685 }
686}
687
688List<String> _validateAssets(Object? yaml) {
689 final (_, List<String> errors) = _computeAssetsSafe(yaml);
690 return errors;
691}
692
693// TODO(andrewkolos): We end up parsing the assets section twice, once during
694// validation and once when the assets getter is called. We should consider
695// refactoring this class to parse and store everything in the constructor.
696// https://github.com/flutter/flutter/issues/139183
697(List<AssetsEntry>, List<String> errors) _computeAssetsSafe(Object? yaml) {
698 if (yaml == null) {
699 return (const <AssetsEntry>[], const <String>[]);
700 }
701 if (yaml is! YamlList) {
702 final String error = 'Expected "assets" to be a list, but got $yaml (${yaml.runtimeType}).';
703 return (const <AssetsEntry>[], <String>[error]);
704 }
705 final List<AssetsEntry> results = <AssetsEntry>[];
706 final List<String> errors = <String>[];
707 for (final Object? rawAssetEntry in yaml) {
708 final (AssetsEntry? parsed, String? error) = AssetsEntry.parseFromYamlSafe(rawAssetEntry);
709 if (parsed != null) {
710 results.add(parsed);
711 }
712 if (error != null) {
713 errors.add(error);
714 }
715 }
716 return (results, errors);
717}
718
719List<AssetsEntry> _computeAssets(Object? assetsSection) {
720 final (List<AssetsEntry> result, List<String> errors) = _computeAssetsSafe(assetsSection);
721 if (errors.isNotEmpty) {
722 throw Exception(
723 'Uncaught error(s) in assets section: '
724 '${errors.join('\n')}',
725 );
726 }
727 return result;
728}
729
730void _validateFonts(YamlList fonts, List<String> errors) {
731 const Set<int> fontWeights = <int>{100, 200, 300, 400, 500, 600, 700, 800, 900};
732 for (final Object? fontMap in fonts) {
733 if (fontMap is! YamlMap) {
734 errors.add('Unexpected child "$fontMap" found under "fonts". Expected a map.');
735 continue;
736 }
737 for (final Object? key in fontMap.keys.where(
738 (Object? key) => key != 'family' && key != 'fonts',
739 )) {
740 errors.add('Unexpected child "$key" found under "fonts".');
741 }
742 if (fontMap['family'] != null && fontMap['family'] is! String) {
743 errors.add('Font family must either be null or a String.');
744 }
745 if (fontMap['fonts'] == null) {
746 continue;
747 } else if (fontMap['fonts'] is! YamlList) {
748 errors.add('Expected "fonts" to either be null or a list.');
749 continue;
750 }
751 for (final Object? fontMapList in fontMap['fonts'] as List<Object?>) {
752 if (fontMapList is! YamlMap) {
753 errors.add('Expected "fonts" to be a list of maps.');
754 continue;
755 }
756 for (final MapEntry<Object?, Object?> kvp in fontMapList.entries) {
757 final Object? fontKey = kvp.key;
758 if (fontKey is! String) {
759 errors.add('Expected "$fontKey" under "fonts" to be a string.');
760 }
761 switch (fontKey) {
762 case 'asset':
763 if (kvp.value is! String) {
764 errors.add(
765 'Expected font asset ${kvp.value} ((${kvp.value.runtimeType})) to be a string.',
766 );
767 }
768 case 'weight':
769 if (!fontWeights.contains(kvp.value)) {
770 errors.add(
771 'Invalid value ${kvp.value} ((${kvp.value.runtimeType})) for font -> weight.',
772 );
773 }
774 case 'style':
775 if (kvp.value != 'normal' && kvp.value != 'italic') {
776 errors.add(
777 'Invalid value ${kvp.value} ((${kvp.value.runtimeType})) for font -> style.',
778 );
779 }
780 default:
781 errors.add('Unexpected key $fontKey ((${kvp.value.runtimeType})) under font.');
782 }
783 }
784 }
785 }
786}
787
788/// Represents an entry under the `assets` section of a pubspec.
789@immutable
790class AssetsEntry {
791 const AssetsEntry({
792 required this.uri,
793 this.flavors = const <String>{},
794 this.transformers = const <AssetTransformerEntry>[],
795 });
796
797 final Uri uri;
798 final Set<String> flavors;
799 final List<AssetTransformerEntry> transformers;
800
801 Object? get descriptor {
802 if (transformers.isEmpty && flavors.isEmpty) {
803 return uri.toString();
804 }
805 return <String, Object?>{
806 _pathKey: uri.toString(),
807 if (flavors.isNotEmpty) _flavorKey: flavors.toList(),
808 if (transformers.isNotEmpty)
809 _transformersKey: transformers.map((AssetTransformerEntry e) => e.descriptor).toList(),
810 };
811 }
812
813 static const String _pathKey = 'path';
814 static const String _flavorKey = 'flavors';
815 static const String _transformersKey = 'transformers';
816
817 static AssetsEntry? parseFromYaml(Object? yaml) {
818 final (AssetsEntry? value, String? error) = parseFromYamlSafe(yaml);
819 if (error != null) {
820 throw Exception('Unexpected error when parsing assets entry');
821 }
822 return value!;
823 }
824
825 static (AssetsEntry? assetsEntry, String? error) parseFromYamlSafe(Object? yaml) {
826 (Uri?, String?) tryParseUri(String uri) {
827 try {
828 return (Uri(pathSegments: uri.split('/')), null);
829 } on FormatException {
830 return (null, 'Asset manifest contains invalid uri: $uri.');
831 }
832 }
833
834 if (yaml == null || yaml == '') {
835 return (null, 'Asset manifest contains a null or empty uri.');
836 }
837
838 if (yaml is String) {
839 final (Uri? uri, String? error) = tryParseUri(yaml);
840 return uri == null ? (null, error) : (AssetsEntry(uri: uri), null);
841 }
842
843 if (yaml is Map) {
844 if (yaml.keys.isEmpty) {
845 return (null, null);
846 }
847
848 final Object? path = yaml[_pathKey];
849
850 if (path == null || path is! String) {
851 return (
852 null,
853 'Asset manifest entry is malformed. '
854 'Expected asset entry to be either a string or a map '
855 'containing a "$_pathKey" entry. Got ${path.runtimeType} instead.',
856 );
857 }
858
859 final (List<String>? flavors, List<String> flavorsErrors) = _parseFlavorsSection(
860 yaml[_flavorKey],
861 );
862 final (
863 List<AssetTransformerEntry>? transformers,
864 List<String> transformersErrors,
865 ) = _parseTransformersSection(yaml[_transformersKey]);
866
867 final List<String> errors = <String>[
868 ...flavorsErrors.map((String e) => 'In $_flavorKey section of asset "$path": $e'),
869 ...transformersErrors.map(
870 (String e) => 'In $_transformersKey section of asset "$path": $e',
871 ),
872 ];
873 if (errors.isNotEmpty) {
874 return (null, <String>['Unable to parse assets section.', ...errors].join('\n'));
875 }
876
877 return (
878 AssetsEntry(
879 uri: Uri(pathSegments: path.split('/')),
880 flavors: Set<String>.from(flavors ?? <String>[]),
881 transformers: transformers ?? <AssetTransformerEntry>[],
882 ),
883 null,
884 );
885 }
886
887 return (
888 null,
889 'Assets entry had unexpected shape. '
890 'Expected a string or an object. Got ${yaml.runtimeType} instead.',
891 );
892 }
893
894 static (List<String>? flavors, List<String> errors) _parseFlavorsSection(Object? yaml) {
895 if (yaml == null) {
896 return (null, <String>[]);
897 }
898
899 return _parseList<String>(yaml, _flavorKey, 'String');
900 }
901
902 static (List<AssetTransformerEntry>?, List<String> errors) _parseTransformersSection(
903 Object? yaml,
904 ) {
905 if (yaml == null) {
906 return (null, <String>[]);
907 }
908 final (List<YamlMap>? yamlObjects, List<String> listErrors) = _parseList<YamlMap>(
909 yaml,
910 '$_transformersKey list',
911 'Map',
912 );
913
914 if (listErrors.isNotEmpty) {
915 return (null, listErrors);
916 }
917
918 final List<AssetTransformerEntry> transformers = <AssetTransformerEntry>[];
919 final List<String> errors = <String>[];
920 for (final YamlMap yaml in yamlObjects!) {
921 final (
922 AssetTransformerEntry? transformerEntry,
923 List<String> transformerErrors,
924 ) = AssetTransformerEntry.tryParse(yaml);
925 if (transformerEntry != null) {
926 transformers.add(transformerEntry);
927 } else {
928 errors.addAll(transformerErrors);
929 }
930 }
931
932 if (errors.isEmpty) {
933 return (transformers, errors);
934 }
935 return (null, errors);
936 }
937
938 @override
939 bool operator ==(Object other) {
940 if (other is! AssetsEntry) {
941 return false;
942 }
943
944 return uri == other.uri && setEquals(flavors, other.flavors);
945 }
946
947 @override
948 int get hashCode => Object.hashAll(<Object?>[
949 uri.hashCode,
950 Object.hashAllUnordered(flavors),
951 Object.hashAll(transformers),
952 ]);
953
954 @override
955 String toString() => 'AssetsEntry(uri: $uri, flavors: $flavors, transformers: $transformers)';
956}
957
958/// Represents an entry in the "transformers" section of an asset.
959@immutable
960final class AssetTransformerEntry {
961 const AssetTransformerEntry({required this.package, required List<String>? args})
962 : args = args ?? const <String>[];
963
964 final String package;
965 final List<String>? args;
966
967 Map<String, Object?> get descriptor {
968 return <String, Object?>{_kPackage: package, if (args != null) _kArgs: args};
969 }
970
971 static const String _kPackage = 'package';
972 static const String _kArgs = 'args';
973
974 static (AssetTransformerEntry? entry, List<String> errors) tryParse(Object? yaml) {
975 if (yaml == null) {
976 return (null, <String>['Transformer entry is null.']);
977 }
978 if (yaml is! YamlMap) {
979 return (null, <String>['Expected entry to be a map. Found ${yaml.runtimeType} instead']);
980 }
981
982 final Object? package = yaml['package'];
983 if (package is! String || package.isEmpty) {
984 return (
985 null,
986 <String>['Expected "package" to be a String. Found ${package.runtimeType} instead.'],
987 );
988 }
989
990 final (List<String>? args, List<String> argsErrors) = _parseArgsSection(yaml['args']);
991 if (argsErrors.isNotEmpty) {
992 return (
993 null,
994 argsErrors
995 .map((String e) => 'In args section of transformer using package "$package": $e')
996 .toList(),
997 );
998 }
999
1000 return (AssetTransformerEntry(package: package, args: args), <String>[]);
1001 }
1002
1003 static (List<String>? args, List<String> errors) _parseArgsSection(Object? yaml) {
1004 if (yaml == null) {
1005 return (null, <String>[]);
1006 }
1007 return _parseList(yaml, 'args', 'String');
1008 }
1009
1010 @override
1011 bool operator ==(Object other) {
1012 if (identical(this, other)) {
1013 return true;
1014 }
1015 if (other is! AssetTransformerEntry) {
1016 return false;
1017 }
1018
1019 final bool argsAreEqual =
1020 (() {
1021 if (args == null && other.args == null) {
1022 return true;
1023 }
1024 if (args?.length != other.args?.length) {
1025 return false;
1026 }
1027
1028 for (int index = 0; index < args!.length; index += 1) {
1029 if (args![index] != other.args![index]) {
1030 return false;
1031 }
1032 }
1033 return true;
1034 })();
1035
1036 return package == other.package && argsAreEqual;
1037 }
1038
1039 @override
1040 int get hashCode =>
1041 Object.hashAll(<Object?>[package.hashCode, args?.map((String e) => e.hashCode)]);
1042
1043 @override
1044 String toString() {
1045 return 'AssetTransformerEntry(package: $package, args: $args)';
1046 }
1047}
1048

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com