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'; |
6 | library; |
7 | |
8 | import 'package:meta/meta.dart'; |
9 | import 'package:pub_semver/pub_semver.dart'; |
10 | import 'package:yaml/yaml.dart'; |
11 | |
12 | import 'base/deferred_component.dart'; |
13 | import 'base/file_system.dart'; |
14 | import 'base/logger.dart'; |
15 | import 'base/utils.dart'; |
16 | import 'globals.dart' as globals; |
17 | import 'plugins.dart'; |
18 | |
19 | const 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. |
29 | class 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 | |
449 | class 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 | |
466 | class 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 | |
491 | bool _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 | |
533 | void _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 | |
655 | void _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 | |
688 | List<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 | |
719 | List<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 | |
730 | void _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 |
790 | class 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 |
960 | final 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 |
Definitions
- _kValidPluginPlatforms
- FlutterManifest
- _
- empty
- createFromPath
- createFromString
- _createFromYaml
- copyWith
- flutterDescriptor
- isEmpty
- appName
- dependencies
- workspace
- appVersion
- buildName
- buildNumber
- usesMaterialDesign
- disabledSwiftPackageManager
- usesAndroidX
- additionalLicenses
- isModule
- isPlugin
- androidPackage
- computeDeferredComponents
- iosBundleIdentifier
- supportedPlatforms
- validSupportedPlatforms
- fontsDescriptor
- _rawFontsDescriptor
- _extractFonts
- _extractAssetUris
- defaultFlavor
- toYaml
- Font
- Font
- descriptor
- toString
- FontAsset
- FontAsset
- descriptor
- toString
- _validate
- _validateFlutter
- _parseList
- _validateDeferredComponents
- _validateAssets
- _computeAssetsSafe
- _computeAssets
- _validateFonts
- AssetsEntry
- AssetsEntry
- descriptor
- parseFromYaml
- parseFromYamlSafe
- tryParseUri
- _parseFlavorsSection
- _parseTransformersSection
- ==
- hashCode
- toString
- AssetTransformerEntry
- AssetTransformerEntry
- descriptor
- tryParse
- _parseArgsSection
- ==
- hashCode
Learn more about Flutter for embedded and desktop on industrialflutter.com