• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2017 The Chromium 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
5import 'package:meta/meta.dart';
6import 'package:pub_semver/pub_semver.dart';
7import 'package:yaml/yaml.dart';
8
9import 'base/file_system.dart';
10import 'base/user_messages.dart';
11import 'base/utils.dart';
12import 'cache.dart';
13import 'globals.dart';
14
15/// A wrapper around the `flutter` section in the `pubspec.yaml` file.
16class FlutterManifest {
17  FlutterManifest._();
18
19  /// Returns an empty manifest.
20  static FlutterManifest empty() {
21    final FlutterManifest manifest = FlutterManifest._();
22    manifest._descriptor = const <String, dynamic>{};
23    manifest._flutterDescriptor = const <String, dynamic>{};
24    return manifest;
25  }
26
27  /// Returns null on invalid manifest. Returns empty manifest on missing file.
28  static FlutterManifest createFromPath(String path) {
29    if (path == null || !fs.isFileSync(path))
30      return _createFromYaml(null);
31    final String manifest = fs.file(path).readAsStringSync();
32    return createFromString(manifest);
33  }
34
35  /// Returns null on missing or invalid manifest
36  @visibleForTesting
37  static FlutterManifest createFromString(String manifest) {
38    return _createFromYaml(loadYaml(manifest));
39  }
40
41  static FlutterManifest _createFromYaml(dynamic yamlDocument) {
42    final FlutterManifest pubspec = FlutterManifest._();
43    if (yamlDocument != null && !_validate(yamlDocument))
44      return null;
45
46    final Map<dynamic, dynamic> yamlMap = yamlDocument;
47    if (yamlMap != null) {
48      pubspec._descriptor = yamlMap.cast<String, dynamic>();
49    } else {
50      pubspec._descriptor = <String, dynamic>{};
51    }
52
53    final Map<dynamic, dynamic> flutterMap = pubspec._descriptor['flutter'];
54    if (flutterMap != null) {
55      pubspec._flutterDescriptor = flutterMap.cast<String, dynamic>();
56    } else {
57      pubspec._flutterDescriptor = <String, dynamic>{};
58    }
59
60    return pubspec;
61  }
62
63  /// A map representation of the entire `pubspec.yaml` file.
64  Map<String, dynamic> _descriptor;
65
66  /// A map representation of the `flutter` section in the `pubspec.yaml` file.
67  Map<String, dynamic> _flutterDescriptor;
68
69  /// True if the `pubspec.yaml` file does not exist.
70  bool get isEmpty => _descriptor.isEmpty;
71
72  /// The string value of the top-level `name` property in the `pubspec.yaml` file.
73  String get appName => _descriptor['name'] ?? '';
74
75  // Flag to avoid printing multiple invalid version messages.
76  bool _hasShowInvalidVersionMsg = false;
77
78  /// The version String from the `pubspec.yaml` file.
79  /// Can be null if it isn't set or has a wrong format.
80  String get appVersion {
81    final String verStr = _descriptor['version']?.toString();
82    if (verStr == null) {
83      return null;
84    }
85
86    Version version;
87    try {
88      version = Version.parse(verStr);
89    } on Exception {
90      if (!_hasShowInvalidVersionMsg) {
91        printStatus(userMessages.invalidVersionSettingHintMessage(verStr), emphasis: true);
92        _hasShowInvalidVersionMsg = true;
93      }
94    }
95    return version?.toString();
96  }
97
98  /// The build version name from the `pubspec.yaml` file.
99  /// Can be null if version isn't set or has a wrong format.
100  String get buildName {
101    if (appVersion != null && appVersion.contains('+'))
102      return appVersion.split('+')?.elementAt(0);
103    else
104      return appVersion;
105  }
106
107  /// The build version number from the `pubspec.yaml` file.
108  /// Can be null if version isn't set or has a wrong format.
109  String get buildNumber {
110    if (appVersion != null && appVersion.contains('+')) {
111      final String value = appVersion.split('+')?.elementAt(1);
112      return value;
113    } else {
114      return null;
115    }
116  }
117
118  bool get usesMaterialDesign {
119    return _flutterDescriptor['uses-material-design'] ?? false;
120  }
121
122  /// True if this Flutter module should use AndroidX dependencies.
123  ///
124  /// If false the deprecated Android Support library will be used.
125  bool get usesAndroidX {
126    return _flutterDescriptor['module']['androidX'] ?? false;
127  }
128
129  /// True if this manifest declares a Flutter module project.
130  ///
131  /// A Flutter project is considered a module when it has a `module:`
132  /// descriptor. A Flutter module project supports integration into an
133  /// existing host app, and has managed platform host code.
134  ///
135  /// Such a project can be created using `flutter create -t module`.
136  bool get isModule => _flutterDescriptor.containsKey('module');
137
138  /// True if this manifest declares a Flutter plugin project.
139  ///
140  /// A Flutter project is considered a plugin when it has a `plugin:`
141  /// descriptor. A Flutter plugin project wraps custom Android and/or
142  /// iOS code in a Dart interface for consumption by other Flutter app
143  /// projects.
144  ///
145  /// Such a project can be created using `flutter create -t plugin`.
146  bool get isPlugin => _flutterDescriptor.containsKey('plugin');
147
148  /// Returns the Android package declared by this manifest in its
149  /// module or plugin descriptor. Returns null, if there is no
150  /// such declaration.
151  String get androidPackage {
152    if (isModule)
153      return _flutterDescriptor['module']['androidPackage'];
154    if (isPlugin)
155      return _flutterDescriptor['plugin']['androidPackage'];
156    return null;
157  }
158
159  /// Returns the iOS bundle identifier declared by this manifest in its
160  /// module descriptor. Returns null if there is no such declaration.
161  String get iosBundleIdentifier {
162    if (isModule)
163      return _flutterDescriptor['module']['iosBundleIdentifier'];
164    return null;
165  }
166
167  List<Map<String, dynamic>> get fontsDescriptor {
168    return fonts.map((Font font) => font.descriptor).toList();
169  }
170
171  List<Map<String, dynamic>> get _rawFontsDescriptor {
172    final List<dynamic> fontList = _flutterDescriptor['fonts'];
173    return fontList == null
174        ? const <Map<String, dynamic>>[]
175        : fontList.map<Map<String, dynamic>>(castStringKeyedMap).toList();
176  }
177
178  List<Uri> get assets {
179    final List<dynamic> assets = _flutterDescriptor['assets'];
180    if (assets == null) {
181      return const <Uri>[];
182    }
183    return assets
184        .cast<String>()
185        .map<String>(Uri.encodeFull)
186        ?.map<Uri>(Uri.parse)
187        ?.toList();
188  }
189
190  List<Font> _fonts;
191
192  List<Font> get fonts {
193    _fonts ??= _extractFonts();
194    return _fonts;
195  }
196
197  List<Font> _extractFonts() {
198    if (!_flutterDescriptor.containsKey('fonts'))
199      return <Font>[];
200
201    final List<Font> fonts = <Font>[];
202    for (Map<String, dynamic> fontFamily in _rawFontsDescriptor) {
203      final List<dynamic> fontFiles = fontFamily['fonts'];
204      final String familyName = fontFamily['family'];
205      if (familyName == null) {
206        printError('Warning: Missing family name for font.', emphasis: true);
207        continue;
208      }
209      if (fontFiles == null) {
210        printError('Warning: No fonts specified for font $familyName', emphasis: true);
211        continue;
212      }
213
214      final List<FontAsset> fontAssets = <FontAsset>[];
215      for (Map<dynamic, dynamic> fontFile in fontFiles) {
216        final String asset = fontFile['asset'];
217        if (asset == null) {
218          printError('Warning: Missing asset in fonts for $familyName', emphasis: true);
219          continue;
220        }
221
222        fontAssets.add(FontAsset(
223          Uri.parse(asset),
224          weight: fontFile['weight'],
225          style: fontFile['style'],
226        ));
227      }
228      if (fontAssets.isNotEmpty)
229        fonts.add(Font(fontFamily['family'], fontAssets));
230    }
231    return fonts;
232  }
233}
234
235class Font {
236  Font(this.familyName, this.fontAssets)
237    : assert(familyName != null),
238      assert(fontAssets != null),
239      assert(fontAssets.isNotEmpty);
240
241  final String familyName;
242  final List<FontAsset> fontAssets;
243
244  Map<String, dynamic> get descriptor {
245    return <String, dynamic>{
246      'family': familyName,
247      'fonts': fontAssets.map<Map<String, dynamic>>((FontAsset a) => a.descriptor).toList(),
248    };
249  }
250
251  @override
252  String toString() => '$runtimeType(family: $familyName, assets: $fontAssets)';
253}
254
255class FontAsset {
256  FontAsset(this.assetUri, {this.weight, this.style})
257    : assert(assetUri != null);
258
259  final Uri assetUri;
260  final int weight;
261  final String style;
262
263  Map<String, dynamic> get descriptor {
264    final Map<String, dynamic> descriptor = <String, dynamic>{};
265    if (weight != null)
266      descriptor['weight'] = weight;
267
268    if (style != null)
269      descriptor['style'] = style;
270
271    descriptor['asset'] = assetUri.path;
272    return descriptor;
273  }
274
275  @override
276  String toString() => '$runtimeType(asset: ${assetUri.path}, weight; $weight, style: $style)';
277}
278
279@visibleForTesting
280String buildSchemaDir(FileSystem fs) {
281  return fs.path.join(
282    fs.path.absolute(Cache.flutterRoot), 'packages', 'flutter_tools', 'schema',
283  );
284}
285
286@visibleForTesting
287String buildSchemaPath(FileSystem fs) {
288  return fs.path.join(
289    buildSchemaDir(fs),
290    'pubspec_yaml.json',
291  );
292}
293
294/// This method should be kept in sync with the schema in
295/// `$FLUTTER_ROOT/packages/flutter_tools/schema/pubspec_yaml.json`,
296/// but avoid introducing depdendencies on packages for simple validation.
297bool _validate(YamlMap manifest) {
298  final List<String> errors = <String>[];
299  for (final MapEntry<dynamic, dynamic> kvp in manifest.entries) {
300    if (kvp.key is! String) {
301      errors.add('Expected YAML key to be a a string, but got ${kvp.key}.');
302      continue;
303    }
304    switch (kvp.key) {
305      case 'name':
306        if (kvp.value is! String) {
307          errors.add('Expected "${kvp.key}" to be a string, but got ${kvp.value}.');
308        }
309        break;
310      case 'flutter':
311        if (kvp.value == null) {
312          continue;
313        }
314        if (kvp.value is! YamlMap) {
315          errors.add('Expected "${kvp.key}" section to be an object or null, but got ${kvp.value}.');
316        }
317        _validateFlutter(kvp.value, errors);
318        break;
319      default:
320        // additionalProperties are allowed.
321        break;
322    }
323  }
324
325  if (errors.isNotEmpty) {
326    printStatus('Error detected in pubspec.yaml:', emphasis: true);
327    printError(errors.join('\n'));
328    return false;
329  }
330
331  return true;
332}
333
334void _validateFlutter(YamlMap yaml, List<String> errors) {
335  if (yaml == null || yaml.entries == null) {
336    return;
337  }
338  for (final MapEntry<dynamic, dynamic> kvp in yaml.entries) {
339    if (kvp.key is! String) {
340      errors.add('Expected YAML key to be a a string, but got ${kvp.key} (${kvp.value.runtimeType}).');
341      continue;
342    }
343    switch (kvp.key) {
344      case 'uses-material-design':
345        if (kvp.value is! bool) {
346          errors.add('Expected "${kvp.key}" to be a bool, but got ${kvp.value} (${kvp.value.runtimeType}).');
347        }
348        break;
349      case 'assets':
350      case 'services':
351        if (kvp.value is! YamlList || kvp.value[0] is! String) {
352          errors.add('Expected "${kvp.key}" to be a list, but got ${kvp.value} (${kvp.value.runtimeType}).');
353        }
354        break;
355      case 'fonts':
356        if (kvp.value is! YamlList || kvp.value[0] is! YamlMap) {
357          errors.add('Expected "${kvp.key}" to be a list, but got ${kvp.value} (${kvp.value.runtimeType}).');
358        } else {
359          _validateFonts(kvp.value, errors);
360        }
361        break;
362      case 'module':
363        if (kvp.value is! YamlMap) {
364          errors.add('Expected "${kvp.key}" to be an object, but got ${kvp.value} (${kvp.value.runtimeType}).');
365        }
366
367        if (kvp.value['androidX'] != null && kvp.value['androidX'] is! bool) {
368          errors.add('The "androidX" value must be a bool if set.');
369        }
370        if (kvp.value['androidPackage'] != null && kvp.value['androidPackage'] is! String) {
371          errors.add('The "androidPackage" value must be a string if set.');
372        }
373        if (kvp.value['iosBundleIdentifier'] != null && kvp.value['iosBundleIdentifier'] is! String) {
374          errors.add('The "iosBundleIdentifier" section must be a string if set.');
375        }
376        break;
377      case 'plugin':
378        if (kvp.value is! YamlMap) {
379          errors.add('Expected "${kvp.key}" to be an object, but got ${kvp.value} (${kvp.value.runtimeType}).');
380        }
381        if (kvp.value['androidPackage'] != null && kvp.value['androidPackage'] is! String) {
382          errors.add('The "androidPackage" must either be null or a string.');
383        }
384        if (kvp.value['iosPrefix'] != null && kvp.value['iosPrefix'] is! String) {
385          errors.add('The "iosPrefix" must either be null or a string.');
386        }
387        if (kvp.value['macosPrefix'] != null && kvp.value['macosPrefix'] is! String) {
388          errors.add('The "macosPrefix" must either be null or a string.');
389        }
390        if (kvp.value['pluginClass'] != null && kvp.value['pluginClass'] is! String) {
391          errors.add('The "pluginClass" must either be null or a string..');
392        }
393        break;
394      default:
395        errors.add('Unexpected child "${kvp.key}" found under "flutter".');
396        break;
397    }
398  }
399}
400
401void _validateFonts(YamlList fonts, List<String> errors) {
402  if (fonts == null) {
403    return;
404  }
405  const Set<int> fontWeights = <int>{
406    100, 200, 300, 400, 500, 600, 700, 800, 900,
407  };
408  for (final dynamic fontListEntry in fonts) {
409    if (fontListEntry is! YamlMap) {
410      errors.add('Unexpected child "$fontListEntry" found under "fonts". Expected a map.');
411      continue;
412    }
413    final YamlMap fontMap = fontListEntry;
414    for (dynamic key in fontMap.keys.where((dynamic key) => key != 'family' && key != 'fonts')) {
415      errors.add('Unexpected child "$key" found under "fonts".');
416    }
417    if (fontMap['family'] != null && fontMap['family'] is! String) {
418      errors.add('Font family must either be null or a String.');
419    }
420    if (fontMap['fonts'] == null) {
421      continue;
422    } else if (fontMap['fonts'] is! YamlList) {
423      errors.add('Expected "fonts" to either be null or a list.');
424      continue;
425    }
426    for (final YamlMap fontListItem in fontMap['fonts']) {
427      for (final MapEntry<dynamic, dynamic> kvp in fontListItem.entries) {
428        if (kvp.key is! String) {
429          errors.add('Expected "${kvp.key}" under "fonts" to be a string.');
430        }
431        switch(kvp.key) {
432          case 'asset':
433            if (kvp.value is! String) {
434              errors.add('Expected font asset ${kvp.value} ((${kvp.value.runtimeType})) to be a string.');
435            }
436            break;
437          case 'weight':
438            if (!fontWeights.contains(kvp.value)) {
439              errors.add('Invalid value ${kvp.value} ((${kvp.value.runtimeType})) for font -> weight.');
440            }
441            break;
442          case 'style':
443            if (kvp.value != 'normal' && kvp.value != 'italic') {
444              errors.add('Invalid value ${kvp.value} ((${kvp.value.runtimeType})) for font -> style.');
445            }
446            break;
447          default:
448            errors.add('Unexpected key ${kvp.key} ((${kvp.value.runtimeType})) under font.');
449            break;
450        }
451      }
452    }
453  }
454}
455