flutter_manifest.dart 14.7 KB
Newer Older
1 2 3 4
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:meta/meta.dart';
6
import 'package:pub_semver/pub_semver.dart';
7 8 9
import 'package:yaml/yaml.dart';

import 'base/file_system.dart';
10
import 'base/user_messages.dart';
11
import 'base/utils.dart';
12
import 'cache.dart';
13
import 'globals.dart';
14

15
/// A wrapper around the `flutter` section in the `pubspec.yaml` file.
16 17 18
class FlutterManifest {
  FlutterManifest._();

19 20
  /// Returns an empty manifest.
  static FlutterManifest empty() {
21
    final FlutterManifest manifest = FlutterManifest._();
22 23 24 25 26
    manifest._descriptor = const <String, dynamic>{};
    manifest._flutterDescriptor = const <String, dynamic>{};
    return manifest;
  }

27
  /// Returns null on invalid manifest. Returns empty manifest on missing file.
28
  static FlutterManifest createFromPath(String path) {
29 30
    if (path == null || !fs.isFileSync(path))
      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 39 40
    return _createFromYaml(loadYaml(manifest));
  }

41
  static FlutterManifest _createFromYaml(dynamic yamlDocument) {
42
    final FlutterManifest pubspec = FlutterManifest._();
43
    if (yamlDocument != null && !_validate(yamlDocument))
44 45
      return null;

46 47 48 49 50 51 52 53 54 55 56 57 58 59
    final Map<dynamic, dynamic> yamlMap = yamlDocument;
    if (yamlMap != null) {
      pubspec._descriptor = yamlMap.cast<String, dynamic>();
    } else {
      pubspec._descriptor = <String, dynamic>{};
    }

    final Map<dynamic, dynamic> flutterMap = pubspec._descriptor['flutter'];
    if (flutterMap != null) {
      pubspec._flutterDescriptor = flutterMap.cast<String, dynamic>();
    } else {
      pubspec._flutterDescriptor = <String, dynamic>{};
    }

60 61 62 63 64 65 66 67 68
    return pubspec;
  }

  /// A map representation of the entire `pubspec.yaml` file.
  Map<String, dynamic> _descriptor;

  /// A map representation of the `flutter` section in the `pubspec.yaml` file.
  Map<String, dynamic> _flutterDescriptor;

69
  /// True if the `pubspec.yaml` file does not exist.
70 71
  bool get isEmpty => _descriptor.isEmpty;

72
  /// The string value of the top-level `name` property in the `pubspec.yaml` file.
73
  String get appName => _descriptor['name'] ?? '';
74

75 76 77
  // Flag to avoid printing multiple invalid version messages.
  bool _hasShowInvalidVersionMsg = false;

78 79 80
  /// The version String from the `pubspec.yaml` file.
  /// Can be null if it isn't set or has a wrong format.
  String get appVersion {
81 82 83 84 85 86 87 88 89 90 91
    final String verStr = _descriptor['version']?.toString();
    if (verStr == null) {
      return null;
    }

    Version version;
    try {
      version = Version.parse(verStr);
    } on Exception {
      if (!_hasShowInvalidVersionMsg) {
        printStatus(userMessages.invalidVersionSettingHintMessage(verStr), emphasis: true);
92 93 94
        _hasShowInvalidVersionMsg = true;
      }
    }
95
    return version?.toString();
96 97 98 99 100 101 102
  }

  /// The build version name from the `pubspec.yaml` file.
  /// Can be null if version isn't set or has a wrong format.
  String get buildName {
    if (appVersion != null && appVersion.contains('+'))
      return appVersion.split('+')?.elementAt(0);
103
    else
104 105 106 107 108
      return appVersion;
  }

  /// The build version number from the `pubspec.yaml` file.
  /// Can be null if version isn't set or has a wrong format.
109
  String get buildNumber {
110 111
    if (appVersion != null && appVersion.contains('+')) {
      final String value = appVersion.split('+')?.elementAt(1);
112
      return value;
113 114 115 116 117
    } else {
      return null;
    }
  }

118 119 120 121
  bool get usesMaterialDesign {
    return _flutterDescriptor['uses-material-design'] ?? false;
  }

122 123 124 125 126 127 128
  /// True if this Flutter module should use AndroidX dependencies.
  ///
  /// If false the deprecated Android Support library will be used.
  bool get usesAndroidX {
    return _flutterDescriptor['module']['androidX'] ?? false;
  }

129
  /// True if this manifest declares a Flutter module project.
130
  ///
131 132 133
  /// 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.
134
  ///
135 136
  /// Such a project can be created using `flutter create -t module`.
  bool get isModule => _flutterDescriptor.containsKey('module');
137 138 139 140 141 142 143 144 145 146 147 148

  /// 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
149
  /// module or plugin descriptor. Returns null, if there is no
150 151
  /// such declaration.
  String get androidPackage {
152 153
    if (isModule)
      return _flutterDescriptor['module']['androidPackage'];
154 155 156 157
    if (isPlugin)
      return _flutterDescriptor['plugin']['androidPackage'];
    return null;
  }
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 163
    if (isModule)
      return _flutterDescriptor['module']['iosBundleIdentifier'];
164 165 166
    return null;
  }

167
  List<Map<String, dynamic>> get fontsDescriptor {
168 169 170 171
    return fonts.map((Font font) => font.descriptor).toList();
  }

  List<Map<String, dynamic>> get _rawFontsDescriptor {
172 173 174 175
    final List<dynamic> fontList = _flutterDescriptor['fonts'];
    return fontList == null
        ? const <Map<String, dynamic>>[]
        : fontList.map<Map<String, dynamic>>(castStringKeyedMap).toList();
176 177
  }

178
  List<Uri> get assets {
179 180 181 182 183 184 185 186 187
    final List<dynamic> assets = _flutterDescriptor['assets'];
    if (assets == null) {
      return const <Uri>[];
    }
    return assets
        .cast<String>()
        .map<String>(Uri.encodeFull)
        ?.map<Uri>(Uri.parse)
        ?.toList();
188 189 190 191 192 193 194 195 196 197 198 199 200 201
  }

  List<Font> _fonts;

  List<Font> get fonts {
    _fonts ??= _extractFonts();
    return _fonts;
  }

  List<Font> _extractFonts() {
    if (!_flutterDescriptor.containsKey('fonts'))
      return <Font>[];

    final List<Font> fonts = <Font>[];
202
    for (Map<String, dynamic> fontFamily in _rawFontsDescriptor) {
203
      final List<dynamic> fontFiles = fontFamily['fonts'];
204 205 206 207 208 209 210 211 212 213 214
      final String familyName = fontFamily['family'];
      if (familyName == null) {
        printError('Warning: Missing family name for font.', emphasis: true);
        continue;
      }
      if (fontFiles == null) {
        printError('Warning: No fonts specified for font $familyName', emphasis: true);
        continue;
      }

      final List<FontAsset> fontAssets = <FontAsset>[];
215
      for (Map<dynamic, dynamic> fontFile in fontFiles) {
216 217 218 219 220 221
        final String asset = fontFile['asset'];
        if (asset == null) {
          printError('Warning: Missing asset in fonts for $familyName', emphasis: true);
          continue;
        }

222
        fontAssets.add(FontAsset(
223
          Uri.parse(asset),
224 225 226 227 228
          weight: fontFile['weight'],
          style: fontFile['style'],
        ));
      }
      if (fontAssets.isNotEmpty)
229
        fonts.add(Font(fontFamily['family'], fontAssets));
230 231 232 233 234 235
    }
    return fonts;
  }
}

class Font {
236 237 238 239
  Font(this.familyName, this.fontAssets)
    : assert(familyName != null),
      assert(fontAssets != null),
      assert(fontAssets.isNotEmpty);
240 241 242 243 244 245 246

  final String familyName;
  final List<FontAsset> fontAssets;

  Map<String, dynamic> get descriptor {
    return <String, dynamic>{
      'family': familyName,
247
      'fonts': fontAssets.map<Map<String, dynamic>>((FontAsset a) => a.descriptor).toList(),
248 249 250 251 252 253 254 255
    };
  }

  @override
  String toString() => '$runtimeType(family: $familyName, assets: $fontAssets)';
}

class FontAsset {
256 257
  FontAsset(this.assetUri, {this.weight, this.style})
    : assert(assetUri != null);
258

259
  final Uri assetUri;
260 261 262 263 264 265 266 267 268 269 270
  final int weight;
  final String style;

  Map<String, dynamic> get descriptor {
    final Map<String, dynamic> descriptor = <String, dynamic>{};
    if (weight != null)
      descriptor['weight'] = weight;

    if (style != null)
      descriptor['style'] = style;

271
    descriptor['asset'] = assetUri.path;
272 273 274 275
    return descriptor;
  }

  @override
276
  String toString() => '$runtimeType(asset: ${assetUri.path}, weight; $weight, style: $style)';
277 278
}

279 280 281
@visibleForTesting
String buildSchemaDir(FileSystem fs) {
  return fs.path.join(
282
    fs.path.absolute(Cache.flutterRoot), 'packages', 'flutter_tools', 'schema',
283 284 285 286 287 288 289
  );
}

@visibleForTesting
String buildSchemaPath(FileSystem fs) {
  return fs.path.join(
    buildSchemaDir(fs),
290 291
    'pubspec_yaml.json',
  );
292 293
}

294 295 296
/// This method should be kept in sync with the schema in
/// `$FLUTTER_ROOT/packages/flutter_tools/schema/pubspec_yaml.json`,
/// but avoid introducing depdendencies on packages for simple validation.
297
bool _validate(YamlMap manifest) {
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323
  final List<String> errors = <String>[];
  for (final MapEntry<dynamic, dynamic> kvp in manifest.entries) {
    if (kvp.key is! String) {
      errors.add('Expected YAML key to be a a string, but got ${kvp.key}.');
      continue;
    }
    switch (kvp.key) {
      case 'name':
        if (kvp.value is! String) {
          errors.add('Expected "${kvp.key}" to be a string, but got ${kvp.value}.');
        }
        break;
      case 'flutter':
        if (kvp.value == null) {
          continue;
        }
        if (kvp.value is! YamlMap) {
          errors.add('Expected "${kvp.key}" section to be an object or null, but got ${kvp.value}.');
        }
        _validateFlutter(kvp.value, errors);
        break;
      default:
        // additionalProperties are allowed.
        break;
    }
  }
324

325
  if (errors.isNotEmpty) {
326
    printStatus('Error detected in pubspec.yaml:', emphasis: true);
327
    printError(errors.join('\n'));
328 329
    return false;
  }
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357

  return true;
}

void _validateFlutter(YamlMap yaml, List<String> errors) {
  if (yaml == null || yaml.entries == null) {
    return;
  }
  for (final MapEntry<dynamic, dynamic> kvp in yaml.entries) {
    if (kvp.key is! String) {
      errors.add('Expected YAML key to be a a string, but got ${kvp.key} (${kvp.value.runtimeType}).');
      continue;
    }
    switch (kvp.key) {
      case 'uses-material-design':
        if (kvp.value is! bool) {
          errors.add('Expected "${kvp.key}" to be a bool, but got ${kvp.value} (${kvp.value.runtimeType}).');
        }
        break;
      case 'assets':
      case 'services':
        if (kvp.value is! YamlList || kvp.value[0] is! String) {
          errors.add('Expected "${kvp.key}" to be a list, but got ${kvp.value} (${kvp.value.runtimeType}).');
        }
        break;
      case 'fonts':
        if (kvp.value is! YamlList || kvp.value[0] is! YamlMap) {
          errors.add('Expected "${kvp.key}" to be a list, but got ${kvp.value} (${kvp.value.runtimeType}).');
358 359
        } else {
          _validateFonts(kvp.value, errors);
360 361 362 363 364 365 366
        }
        break;
      case 'module':
        if (kvp.value is! YamlMap) {
          errors.add('Expected "${kvp.key}" to be an object, but got ${kvp.value} (${kvp.value.runtimeType}).');
        }

367 368 369
        if (kvp.value['androidX'] != null && kvp.value['androidX'] is! bool) {
          errors.add('The "androidX" value must be a bool if set.');
        }
370 371 372 373 374 375 376 377 378 379 380 381 382 383 384
        if (kvp.value['androidPackage'] != null && kvp.value['androidPackage'] is! String) {
          errors.add('The "androidPackage" value must be a string if set.');
        }
        if (kvp.value['iosBundleIdentifier'] != null && kvp.value['iosBundleIdentifier'] is! String) {
          errors.add('The "iosBundleIdentifier" section must be a string if set.');
        }
        break;
      case 'plugin':
        if (kvp.value is! YamlMap) {
          errors.add('Expected "${kvp.key}" to be an object, but got ${kvp.value} (${kvp.value.runtimeType}).');
        }
        if (kvp.value['androidPackage'] != null && kvp.value['androidPackage'] is! String) {
          errors.add('The "androidPackage" must either be null or a string.');
        }
        if (kvp.value['iosPrefix'] != null && kvp.value['iosPrefix'] is! String) {
385 386 387 388
          errors.add('The "iosPrefix" must either be null or a string.');
        }
        if (kvp.value['macosPrefix'] != null && kvp.value['macosPrefix'] is! String) {
          errors.add('The "macosPrefix" must either be null or a string.');
389 390 391 392 393 394 395 396 397 398
        }
        if (kvp.value['pluginClass'] != null && kvp.value['pluginClass'] is! String) {
          errors.add('The "pluginClass" must either be null or a string..');
        }
        break;
      default:
        errors.add('Unexpected child "${kvp.key}" found under "flutter".');
        break;
    }
  }
399
}
400 401 402 403 404

void _validateFonts(YamlList fonts, List<String> errors) {
  if (fonts == null) {
    return;
  }
405
  const Set<int> fontWeights = <int>{
406
    100, 200, 300, 400, 500, 600, 700, 800, 900,
407
  };
408 409 410 411 412 413
  for (final dynamic fontListEntry in fonts) {
    if (fontListEntry is! YamlMap) {
      errors.add('Unexpected child "$fontListEntry" found under "fonts". Expected a map.');
      continue;
    }
    final YamlMap fontMap = fontListEntry;
414 415 416 417 418 419 420 421
    for (dynamic key in fontMap.keys.where((dynamic key) => key != 'family' && key != 'fonts')) {
      errors.add('Unexpected child "$key" found under "fonts".');
    }
    if (fontMap['family'] != null && fontMap['family'] is! String) {
      errors.add('Font family must either be null or a String.');
    }
    if (fontMap['fonts'] == null) {
      continue;
422 423 424
    } else if (fontMap['fonts'] is! YamlList) {
      errors.add('Expected "fonts" to either be null or a list.');
      continue;
425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453
    }
    for (final YamlMap fontListItem in fontMap['fonts']) {
      for (final MapEntry<dynamic, dynamic> kvp in fontListItem.entries) {
        if (kvp.key is! String) {
          errors.add('Expected "${kvp.key}" under "fonts" to be a string.');
        }
        switch(kvp.key) {
          case 'asset':
            if (kvp.value is! String) {
              errors.add('Expected font asset ${kvp.value} ((${kvp.value.runtimeType})) to be a string.');
            }
            break;
          case 'weight':
            if (!fontWeights.contains(kvp.value)) {
              errors.add('Invalid value ${kvp.value} ((${kvp.value.runtimeType})) for font -> weight.');
            }
            break;
          case 'style':
            if (kvp.value != 'normal' && kvp.value != 'italic') {
              errors.add('Invalid value ${kvp.value} ((${kvp.value.runtimeType})) for font -> style.');
            }
            break;
          default:
            errors.add('Unexpected key ${kvp.key} ((${kvp.value.runtimeType})) under font.');
            break;
        }
      }
    }
  }
454
}