• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2016 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 'dart:async';
6
7import 'package:yaml/yaml.dart';
8
9import 'base/context.dart';
10import 'base/file_system.dart';
11import 'base/platform.dart';
12import 'base/utils.dart';
13import 'build_info.dart';
14import 'cache.dart';
15import 'convert.dart';
16import 'dart/package_map.dart';
17import 'devfs.dart';
18import 'flutter_manifest.dart';
19import 'globals.dart';
20
21const AssetBundleFactory _kManifestFactory = _ManifestAssetBundleFactory();
22
23const String defaultManifestPath = 'pubspec.yaml';
24
25/// Injected factory class for spawning [AssetBundle] instances.
26abstract class AssetBundleFactory {
27  /// The singleton instance, pulled from the [AppContext].
28  static AssetBundleFactory get instance => context.get<AssetBundleFactory>();
29
30  static AssetBundleFactory get defaultInstance => _kManifestFactory;
31
32  /// Creates a new [AssetBundle].
33  AssetBundle createBundle();
34}
35
36abstract class AssetBundle {
37  Map<String, DevFSContent> get entries;
38
39  bool wasBuiltOnce();
40
41  bool needsBuild({ String manifestPath = defaultManifestPath });
42
43  /// Returns 0 for success; non-zero for failure.
44  Future<int> build({
45    String manifestPath = defaultManifestPath,
46    String assetDirPath,
47    String packagesPath,
48    bool includeDefaultFonts = true,
49    bool reportLicensedPackages = false,
50  });
51}
52
53class _ManifestAssetBundleFactory implements AssetBundleFactory {
54  const _ManifestAssetBundleFactory();
55
56  @override
57  AssetBundle createBundle() => _ManifestAssetBundle();
58}
59
60class _ManifestAssetBundle implements AssetBundle {
61  /// Constructs an [_ManifestAssetBundle] that gathers the set of assets from the
62  /// pubspec.yaml manifest.
63  _ManifestAssetBundle();
64
65  @override
66  final Map<String, DevFSContent> entries = <String, DevFSContent>{};
67
68  // If an asset corresponds to a wildcard directory, then it may have been
69  // updated without changes to the manifest.
70  final Map<Uri, Directory> _wildcardDirectories = <Uri, Directory>{};
71
72  DateTime _lastBuildTimestamp;
73
74  static const String _assetManifestJson = 'AssetManifest.json';
75  static const String _fontManifestJson = 'FontManifest.json';
76  static const String _fontSetMaterial = 'material';
77  static const String _license = 'LICENSE';
78
79  @override
80  bool wasBuiltOnce() => _lastBuildTimestamp != null;
81
82  @override
83  bool needsBuild({ String manifestPath = defaultManifestPath }) {
84    if (_lastBuildTimestamp == null)
85      return true;
86
87    final FileStat stat = fs.file(manifestPath).statSync();
88    if (stat.type == FileSystemEntityType.notFound)
89      return true;
90
91    for (Directory directory in _wildcardDirectories.values) {
92      final DateTime dateTime = directory.statSync().modified;
93      if (dateTime == null) {
94        continue;
95      }
96      if (dateTime.isAfter(_lastBuildTimestamp)) {
97        return true;
98      }
99    }
100
101    return stat.modified.isAfter(_lastBuildTimestamp);
102  }
103
104  @override
105  Future<int> build({
106    String manifestPath = defaultManifestPath,
107    String assetDirPath,
108    String packagesPath,
109    bool includeDefaultFonts = true,
110    bool reportLicensedPackages = false,
111  }) async {
112    assetDirPath ??= getAssetBuildDirectory();
113    packagesPath ??= fs.path.absolute(PackageMap.globalPackagesPath);
114    FlutterManifest flutterManifest;
115    try {
116      flutterManifest = FlutterManifest.createFromPath(manifestPath);
117    } catch (e) {
118      printStatus('Error detected in pubspec.yaml:', emphasis: true);
119      printError('$e');
120      return 1;
121    }
122    if (flutterManifest == null)
123      return 1;
124
125    // If the last build time isn't set before this early return, empty pubspecs will
126    // hang on hot reload, as the incremental dill files will never be copied to the
127    // device.
128    _lastBuildTimestamp = DateTime.now();
129    if (flutterManifest.isEmpty) {
130      entries[_assetManifestJson] = DevFSStringContent('{}');
131      return 0;
132    }
133
134    final String assetBasePath = fs.path.dirname(fs.path.absolute(manifestPath));
135
136    final PackageMap packageMap = PackageMap(packagesPath);
137    final List<Uri> wildcardDirectories = <Uri>[];
138
139    // The _assetVariants map contains an entry for each asset listed
140    // in the pubspec.yaml file's assets and font and sections. The
141    // value of each image asset is a list of resolution-specific "variants",
142    // see _AssetDirectoryCache.
143    final Map<_Asset, List<_Asset>> assetVariants = _parseAssets(
144      packageMap,
145      flutterManifest,
146      wildcardDirectories,
147      assetBasePath,
148      excludeDirs: <String>[assetDirPath, getBuildDirectory()],
149    );
150
151    if (assetVariants == null) {
152      return 1;
153    }
154
155    final List<Map<String, dynamic>> fonts = _parseFonts(
156      flutterManifest,
157      includeDefaultFonts,
158      packageMap,
159    );
160
161    // Add fonts and assets from packages.
162    for (String packageName in packageMap.map.keys) {
163      final Uri package = packageMap.map[packageName];
164      if (package != null && package.scheme == 'file') {
165        final String packageManifestPath = fs.path.fromUri(package.resolve('../pubspec.yaml'));
166        final FlutterManifest packageFlutterManifest = FlutterManifest.createFromPath(packageManifestPath);
167        if (packageFlutterManifest == null)
168          continue;
169        // Skip the app itself
170        if (packageFlutterManifest.appName == flutterManifest.appName)
171          continue;
172        final String packageBasePath = fs.path.dirname(packageManifestPath);
173
174        final Map<_Asset, List<_Asset>> packageAssets = _parseAssets(
175          packageMap,
176          packageFlutterManifest,
177          wildcardDirectories,
178          packageBasePath,
179          packageName: packageName,
180        );
181
182        if (packageAssets == null)
183          return 1;
184        assetVariants.addAll(packageAssets);
185
186        fonts.addAll(_parseFonts(
187          packageFlutterManifest,
188          includeDefaultFonts,
189          packageMap,
190          packageName: packageName,
191        ));
192      }
193    }
194
195    // Save the contents of each image, image variant, and font
196    // asset in entries.
197    for (_Asset asset in assetVariants.keys) {
198      if (!asset.assetFileExists && assetVariants[asset].isEmpty) {
199        printStatus('Error detected in pubspec.yaml:', emphasis: true);
200        printError('No file or variants found for $asset.\n');
201        return 1;
202      }
203      // The file name for an asset's "main" entry is whatever appears in
204      // the pubspec.yaml file. The main entry's file must always exist for
205      // font assets. It need not exist for an image if resolution-specific
206      // variant files exist. An image's main entry is treated the same as a
207      // "1x" resolution variant and if both exist then the explicit 1x
208      // variant is preferred.
209      if (asset.assetFileExists) {
210        assert(!assetVariants[asset].contains(asset));
211        assetVariants[asset].insert(0, asset);
212      }
213      for (_Asset variant in assetVariants[asset]) {
214        assert(variant.assetFileExists);
215        entries[variant.entryUri.path] ??= DevFSFileContent(variant.assetFile);
216      }
217    }
218
219    final List<_Asset> materialAssets = <_Asset>[
220      if (flutterManifest.usesMaterialDesign && includeDefaultFonts)
221        ..._getMaterialAssets(_fontSetMaterial),
222    ];
223    for (_Asset asset in materialAssets) {
224      assert(asset.assetFileExists);
225      entries[asset.entryUri.path] ??= DevFSFileContent(asset.assetFile);
226    }
227
228    // Update wildcard directories we we can detect changes in them.
229    for (Uri uri in wildcardDirectories) {
230      _wildcardDirectories[uri] ??= fs.directory(uri);
231    }
232
233    entries[_assetManifestJson] = _createAssetManifest(assetVariants);
234
235    entries[_fontManifestJson] = DevFSStringContent(json.encode(fonts));
236
237    // TODO(ianh): Only do the following line if we've changed packages or if our LICENSE file changed
238    entries[_license] = await _obtainLicenses(packageMap, assetBasePath, reportPackages: reportLicensedPackages);
239
240    return 0;
241  }
242}
243
244class _Asset {
245  _Asset({ this.baseDir, this.relativeUri, this.entryUri });
246
247  final String baseDir;
248
249  /// A platform-independent Uri where this asset can be found on disk on the
250  /// host system relative to [baseDir].
251  final Uri relativeUri;
252
253  /// A platform-independent Uri representing the entry for the asset manifest.
254  final Uri entryUri;
255
256  File get assetFile {
257    return fs.file(fs.path.join(baseDir, fs.path.fromUri(relativeUri)));
258  }
259
260  bool get assetFileExists => assetFile.existsSync();
261
262  /// The delta between what the entryUri is and the relativeUri (e.g.,
263  /// packages/flutter_gallery).
264  Uri get symbolicPrefixUri {
265    if (entryUri == relativeUri)
266      return null;
267    final int index = entryUri.path.indexOf(relativeUri.path);
268    return index == -1 ? null : Uri(path: entryUri.path.substring(0, index));
269  }
270
271  @override
272  String toString() => 'asset: $entryUri';
273
274  @override
275  bool operator ==(dynamic other) {
276    if (identical(other, this))
277      return true;
278    if (other.runtimeType != runtimeType)
279      return false;
280    final _Asset otherAsset = other;
281    return otherAsset.baseDir == baseDir
282        && otherAsset.relativeUri == relativeUri
283        && otherAsset.entryUri == entryUri;
284  }
285
286  @override
287  int get hashCode {
288    return baseDir.hashCode
289        ^ relativeUri.hashCode
290        ^ entryUri.hashCode;
291  }
292}
293
294Map<String, dynamic> _readMaterialFontsManifest() {
295  final String fontsPath = fs.path.join(fs.path.absolute(Cache.flutterRoot),
296      'packages', 'flutter_tools', 'schema', 'material_fonts.yaml');
297
298  return castStringKeyedMap(loadYaml(fs.file(fontsPath).readAsStringSync()));
299}
300
301final Map<String, dynamic> _materialFontsManifest = _readMaterialFontsManifest();
302
303List<Map<String, dynamic>> _getMaterialFonts(String fontSet) {
304  final List<dynamic> fontsList = _materialFontsManifest[fontSet];
305  return fontsList?.map<Map<String, dynamic>>(castStringKeyedMap)?.toList();
306}
307
308List<_Asset> _getMaterialAssets(String fontSet) {
309  final List<_Asset> result = <_Asset>[];
310
311  for (Map<String, dynamic> family in _getMaterialFonts(fontSet)) {
312    for (Map<dynamic, dynamic> font in family['fonts']) {
313      final Uri entryUri = fs.path.toUri(font['asset']);
314      result.add(_Asset(
315        baseDir: fs.path.join(Cache.flutterRoot, 'bin', 'cache', 'artifacts', 'material_fonts'),
316        relativeUri: Uri(path: entryUri.pathSegments.last),
317        entryUri: entryUri,
318      ));
319    }
320  }
321
322  return result;
323}
324
325final String _licenseSeparator = '\n' + ('-' * 80) + '\n';
326
327/// Returns a DevFSContent representing the license file.
328Future<DevFSContent> _obtainLicenses(
329  PackageMap packageMap,
330  String assetBase, {
331  bool reportPackages,
332}) async {
333  // Read the LICENSE file from each package in the .packages file, splitting
334  // each one into each component license (so that we can de-dupe if possible).
335  //
336  // Individual licenses inside each LICENSE file should be separated by 80
337  // hyphens on their own on a line.
338  //
339  // If a LICENSE file contains more than one component license, then each
340  // component license must start with the names of the packages to which the
341  // component license applies, with each package name on its own line, and the
342  // list of package names separated from the actual license text by a blank
343  // line. (The packages need not match the names of the pub package. For
344  // example, a package might itself contain code from multiple third-party
345  // sources, and might need to include a license for each one.)
346  final Map<String, Set<String>> packageLicenses = <String, Set<String>>{};
347  final Set<String> allPackages = <String>{};
348  for (String packageName in packageMap.map.keys) {
349    final Uri package = packageMap.map[packageName];
350    if (package != null && package.scheme == 'file') {
351      final File file = fs.file(package.resolve('../LICENSE'));
352      if (file.existsSync()) {
353        final List<String> rawLicenses =
354            (await file.readAsString()).split(_licenseSeparator);
355        for (String rawLicense in rawLicenses) {
356          List<String> packageNames;
357          String licenseText;
358          if (rawLicenses.length > 1) {
359            final int split = rawLicense.indexOf('\n\n');
360            if (split >= 0) {
361              packageNames = rawLicense.substring(0, split).split('\n');
362              licenseText = rawLicense.substring(split + 2);
363            }
364          }
365          if (licenseText == null) {
366            packageNames = <String>[packageName];
367            licenseText = rawLicense;
368          }
369          packageLicenses.putIfAbsent(licenseText, () => <String>{})
370            ..addAll(packageNames);
371          allPackages.addAll(packageNames);
372        }
373      }
374    }
375  }
376
377  if (reportPackages) {
378    final List<String> allPackagesList = allPackages.toList()..sort();
379    printStatus('Licenses were found for the following packages:');
380    printStatus(allPackagesList.join(', '));
381  }
382
383  final List<String> combinedLicensesList = packageLicenses.keys.map<String>(
384    (String license) {
385      final List<String> packageNames = packageLicenses[license].toList()
386       ..sort();
387      return packageNames.join('\n') + '\n\n' + license;
388    }
389  ).toList();
390  combinedLicensesList.sort();
391
392  final String combinedLicenses = combinedLicensesList.join(_licenseSeparator);
393
394  return DevFSStringContent(combinedLicenses);
395}
396
397int _byBasename(_Asset a, _Asset b) {
398  return a.assetFile.basename.compareTo(b.assetFile.basename);
399}
400
401DevFSContent _createAssetManifest(Map<_Asset, List<_Asset>> assetVariants) {
402  final Map<String, List<String>> jsonObject = <String, List<String>>{};
403
404  // necessary for making unit tests deterministic
405  final List<_Asset> sortedKeys = assetVariants
406      .keys.toList()
407    ..sort(_byBasename);
408
409  for (_Asset main in sortedKeys) {
410    final List<String> variants = <String>[];
411    for (_Asset variant in assetVariants[main])
412      variants.add(variant.entryUri.path);
413    jsonObject[main.entryUri.path] = variants;
414  }
415  return DevFSStringContent(json.encode(jsonObject));
416}
417
418List<Map<String, dynamic>> _parseFonts(
419  FlutterManifest manifest,
420  bool includeDefaultFonts,
421  PackageMap packageMap, {
422  String packageName,
423}) {
424  return <Map<String, dynamic>>[
425    if (manifest.usesMaterialDesign && includeDefaultFonts)
426      ..._getMaterialFonts(_ManifestAssetBundle._fontSetMaterial),
427    if (packageName == null)
428      ...manifest.fontsDescriptor
429    else
430      ..._createFontsDescriptor(_parsePackageFonts(
431        manifest,
432        packageName,
433        packageMap,
434      )),
435  ];
436}
437
438/// Prefixes family names and asset paths of fonts included from packages with
439/// 'packages/<package_name>'
440List<Font> _parsePackageFonts(
441  FlutterManifest manifest,
442  String packageName,
443  PackageMap packageMap,
444) {
445  final List<Font> packageFonts = <Font>[];
446  for (Font font in manifest.fonts) {
447    final List<FontAsset> packageFontAssets = <FontAsset>[];
448    for (FontAsset fontAsset in font.fontAssets) {
449      final Uri assetUri = fontAsset.assetUri;
450      if (assetUri.pathSegments.first == 'packages' &&
451          !fs.isFileSync(fs.path.fromUri(packageMap.map[packageName].resolve('../${assetUri.path}')))) {
452        packageFontAssets.add(FontAsset(
453          fontAsset.assetUri,
454          weight: fontAsset.weight,
455          style: fontAsset.style,
456        ));
457      } else {
458        packageFontAssets.add(FontAsset(
459          Uri(pathSegments: <String>['packages', packageName, ...assetUri.pathSegments]),
460          weight: fontAsset.weight,
461          style: fontAsset.style,
462        ));
463      }
464    }
465    packageFonts.add(Font('packages/$packageName/${font.familyName}', packageFontAssets));
466  }
467  return packageFonts;
468}
469
470List<Map<String, dynamic>> _createFontsDescriptor(List<Font> fonts) {
471  return fonts.map<Map<String, dynamic>>((Font font) => font.descriptor).toList();
472}
473
474// Given an assets directory like this:
475//
476// assets/foo
477// assets/var1/foo
478// assets/var2/foo
479// assets/bar
480//
481// variantsFor('assets/foo') => ['/assets/var1/foo', '/assets/var2/foo']
482// variantsFor('assets/bar') => []
483class _AssetDirectoryCache {
484  _AssetDirectoryCache(Iterable<String> excluded) {
485    _excluded = excluded.map<String>((String path) => fs.path.absolute(path) + fs.path.separator);
486  }
487
488  Iterable<String> _excluded;
489  final Map<String, Map<String, List<String>>> _cache = <String, Map<String, List<String>>>{};
490
491  List<String> variantsFor(String assetPath) {
492    final String assetName = fs.path.basename(assetPath);
493    final String directory = fs.path.dirname(assetPath);
494
495    if (!fs.directory(directory).existsSync())
496      return const <String>[];
497
498    if (_cache[directory] == null) {
499      final List<String> paths = <String>[];
500      for (FileSystemEntity entity in fs.directory(directory).listSync(recursive: true)) {
501        final String path = entity.path;
502        if (fs.isFileSync(path) && !_excluded.any((String exclude) => path.startsWith(exclude)))
503          paths.add(path);
504      }
505
506      final Map<String, List<String>> variants = <String, List<String>>{};
507      for (String path in paths) {
508        final String variantName = fs.path.basename(path);
509        if (directory == fs.path.dirname(path))
510          continue;
511        variants[variantName] ??= <String>[];
512        variants[variantName].add(path);
513      }
514      _cache[directory] = variants;
515    }
516
517    return _cache[directory][assetName] ?? const <String>[];
518  }
519}
520
521/// Given an assetBase location and a pubspec.yaml Flutter manifest, return a
522/// map of assets to asset variants.
523///
524/// Returns null on missing assets.
525///
526/// Given package: 'test_package' and an assets directory like this:
527///
528/// assets/foo
529/// assets/var1/foo
530/// assets/var2/foo
531/// assets/bar
532///
533/// returns
534/// {
535///   asset: packages/test_package/assets/foo: [
536///     asset: packages/test_package/assets/foo,
537///     asset: packages/test_package/assets/var1/foo,
538///     asset: packages/test_package/assets/var2/foo,
539///   ],
540///   asset: packages/test_package/assets/bar: [
541///     asset: packages/test_package/assets/bar,
542///   ],
543/// }
544///
545
546Map<_Asset, List<_Asset>> _parseAssets(
547  PackageMap packageMap,
548  FlutterManifest flutterManifest,
549  List<Uri> wildcardDirectories,
550  String assetBase, {
551  List<String> excludeDirs = const <String>[],
552  String packageName,
553}) {
554  final Map<_Asset, List<_Asset>> result = <_Asset, List<_Asset>>{};
555
556  final _AssetDirectoryCache cache = _AssetDirectoryCache(excludeDirs);
557  for (Uri assetUri in flutterManifest.assets) {
558    if (assetUri.toString().endsWith('/')) {
559      wildcardDirectories.add(assetUri);
560      _parseAssetsFromFolder(packageMap, flutterManifest, assetBase,
561          cache, result, assetUri,
562          excludeDirs: excludeDirs, packageName: packageName);
563    } else {
564      _parseAssetFromFile(packageMap, flutterManifest, assetBase,
565          cache, result, assetUri,
566          excludeDirs: excludeDirs, packageName: packageName);
567    }
568  }
569
570  // Add assets referenced in the fonts section of the manifest.
571  for (Font font in flutterManifest.fonts) {
572    for (FontAsset fontAsset in font.fontAssets) {
573      final _Asset baseAsset = _resolveAsset(
574        packageMap,
575        assetBase,
576        fontAsset.assetUri,
577        packageName,
578      );
579      if (!baseAsset.assetFileExists) {
580        printError('Error: unable to locate asset entry in pubspec.yaml: "${fontAsset.assetUri}".');
581        return null;
582      }
583
584      result[baseAsset] = <_Asset>[];
585    }
586  }
587
588  return result;
589}
590
591void _parseAssetsFromFolder(
592  PackageMap packageMap,
593  FlutterManifest flutterManifest,
594  String assetBase,
595  _AssetDirectoryCache cache,
596  Map<_Asset, List<_Asset>> result,
597  Uri assetUri, {
598  List<String> excludeDirs = const <String>[],
599  String packageName,
600}) {
601  final String directoryPath = fs.path.join(
602      assetBase, assetUri.toFilePath(windows: platform.isWindows));
603
604  if (!fs.directory(directoryPath).existsSync()) {
605    printError('Error: unable to find directory entry in pubspec.yaml: $directoryPath');
606    return;
607  }
608
609  final List<FileSystemEntity> lister = fs.directory(directoryPath).listSync();
610
611  for (FileSystemEntity entity in lister) {
612    if (entity is File) {
613      final String relativePath = fs.path.relative(entity.path, from: assetBase);
614
615      final Uri uri = Uri.file(relativePath, windows: platform.isWindows);
616
617      _parseAssetFromFile(packageMap, flutterManifest, assetBase, cache, result,
618          uri, packageName: packageName);
619    }
620  }
621}
622
623void _parseAssetFromFile(
624  PackageMap packageMap,
625  FlutterManifest flutterManifest,
626  String assetBase,
627  _AssetDirectoryCache cache,
628  Map<_Asset, List<_Asset>> result,
629  Uri assetUri, {
630  List<String> excludeDirs = const <String>[],
631  String packageName,
632}) {
633  final _Asset asset = _resolveAsset(
634    packageMap,
635    assetBase,
636    assetUri,
637    packageName,
638  );
639  final List<_Asset> variants = <_Asset>[];
640  for (String path in cache.variantsFor(asset.assetFile.path)) {
641    final String relativePath = fs.path.relative(path, from: asset.baseDir);
642    final Uri relativeUri = fs.path.toUri(relativePath);
643    final Uri entryUri = asset.symbolicPrefixUri == null
644        ? relativeUri
645        : asset.symbolicPrefixUri.resolveUri(relativeUri);
646
647    variants.add(
648      _Asset(
649        baseDir: asset.baseDir,
650        entryUri: entryUri,
651        relativeUri: relativeUri,
652        )
653    );
654  }
655
656  result[asset] = variants;
657}
658
659_Asset _resolveAsset(
660  PackageMap packageMap,
661  String assetsBaseDir,
662  Uri assetUri,
663  String packageName,
664) {
665  final String assetPath = fs.path.fromUri(assetUri);
666  if (assetUri.pathSegments.first == 'packages' && !fs.isFileSync(fs.path.join(assetsBaseDir, assetPath))) {
667    // The asset is referenced in the pubspec.yaml as
668    // 'packages/PACKAGE_NAME/PATH/TO/ASSET .
669    final _Asset packageAsset = _resolvePackageAsset(assetUri, packageMap);
670    if (packageAsset != null)
671      return packageAsset;
672  }
673
674  return _Asset(
675    baseDir: assetsBaseDir,
676    entryUri: packageName == null
677        ? assetUri // Asset from the current application.
678        : Uri(pathSegments: <String>['packages', packageName, ...assetUri.pathSegments]), // Asset from, and declared in $packageName.
679    relativeUri: assetUri,
680  );
681}
682
683_Asset _resolvePackageAsset(Uri assetUri, PackageMap packageMap) {
684  assert(assetUri.pathSegments.first == 'packages');
685  if (assetUri.pathSegments.length > 1) {
686    final String packageName = assetUri.pathSegments[1];
687    final Uri packageUri = packageMap.map[packageName];
688    if (packageUri != null && packageUri.scheme == 'file') {
689      return _Asset(
690        baseDir: fs.path.fromUri(packageUri),
691        entryUri: assetUri,
692        relativeUri: Uri(pathSegments: assetUri.pathSegments.sublist(2)),
693      );
694    }
695  }
696  printStatus('Error detected in pubspec.yaml:', emphasis: true);
697  printError('Could not resolve package for asset $assetUri.\n');
698  return null;
699}
700