asset.dart 22.9 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:yaml/yaml.dart';

9
import 'base/context.dart';
10
import 'base/file_system.dart';
11
import 'base/utils.dart';
12
import 'build_info.dart';
13
import 'cache.dart';
14
import 'convert.dart';
15
import 'dart/package_map.dart';
16
import 'devfs.dart';
17
import 'flutter_manifest.dart';
18
import 'globals.dart' as globals;
19

20
const AssetBundleFactory _kManifestFactory = _ManifestAssetBundleFactory();
21

22 23
const String defaultManifestPath = 'pubspec.yaml';

24 25 26
/// Injected factory class for spawning [AssetBundle] instances.
abstract class AssetBundleFactory {
  /// The singleton instance, pulled from the [AppContext].
27
  static AssetBundleFactory get instance => context.get<AssetBundleFactory>();
28 29

  static AssetBundleFactory get defaultInstance => _kManifestFactory;
30 31 32 33 34 35 36 37

  /// Creates a new [AssetBundle].
  AssetBundle createBundle();
}

abstract class AssetBundle {
  Map<String, DevFSContent> get entries;

38 39
  bool wasBuiltOnce();

40
  bool needsBuild({ String manifestPath = defaultManifestPath });
41 42 43

  /// Returns 0 for success; non-zero for failure.
  Future<int> build({
44
    String manifestPath = defaultManifestPath,
45
    String assetDirPath,
46
    String packagesPath,
47
    bool includeDefaultFonts = true,
48
    bool reportLicensedPackages = false,
49 50 51 52 53 54 55
  });
}

class _ManifestAssetBundleFactory implements AssetBundleFactory {
  const _ManifestAssetBundleFactory();

  @override
56
  AssetBundle createBundle() => _ManifestAssetBundle();
57 58 59
}

class _ManifestAssetBundle implements AssetBundle {
60 61 62 63
  /// Constructs an [_ManifestAssetBundle] that gathers the set of assets from the
  /// pubspec.yaml manifest.
  _ManifestAssetBundle();

64
  @override
65
  final Map<String, DevFSContent> entries = <String, DevFSContent>{};
66

67 68 69 70
  // If an asset corresponds to a wildcard directory, then it may have been
  // updated without changes to the manifest.
  final Map<Uri, Directory> _wildcardDirectories = <Uri, Directory>{};

71 72
  DateTime _lastBuildTimestamp;

73 74 75 76
  static const String _assetManifestJson = 'AssetManifest.json';
  static const String _fontManifestJson = 'FontManifest.json';
  static const String _fontSetMaterial = 'material';
  static const String _license = 'LICENSE';
77

78 79 80
  @override
  bool wasBuiltOnce() => _lastBuildTimestamp != null;

81
  @override
82
  bool needsBuild({ String manifestPath = defaultManifestPath }) {
83
    if (_lastBuildTimestamp == null) {
84
      return true;
85
    }
86

87
    final FileStat stat = globals.fs.file(manifestPath).statSync();
88
    if (stat.type == FileSystemEntityType.notFound) {
89
      return true;
90
    }
91

92
    for (Directory directory in _wildcardDirectories.values) {
93 94
      if (!directory.existsSync()) {
        return true; // directory was deleted.
95
      }
96
      for (File file in directory.listSync().whereType<File>()) {
97 98 99 100 101 102 103
        final DateTime dateTime = file.statSync().modified;
        if (dateTime == null) {
          continue;
        }
        if (dateTime.isAfter(_lastBuildTimestamp)) {
          return true;
        }
104 105 106
      }
    }

107 108 109
    return stat.modified.isAfter(_lastBuildTimestamp);
  }

110
  @override
111
  Future<int> build({
112
    String manifestPath = defaultManifestPath,
113
    String assetDirPath,
114
    String packagesPath,
115
    bool includeDefaultFonts = true,
116
    bool reportLicensedPackages = false,
117
  }) async {
118
    assetDirPath ??= getAssetBuildDirectory();
119
    packagesPath ??= globals.fs.path.absolute(PackageMap.globalPackagesPath);
120
    FlutterManifest flutterManifest;
121
    try {
122
      flutterManifest = FlutterManifest.createFromPath(manifestPath);
123
    } catch (e) {
124 125
      globals.printStatus('Error detected in pubspec.yaml:', emphasis: true);
      globals.printError('$e');
126 127
      return 1;
    }
128
    if (flutterManifest == null) {
129
      return 1;
130
    }
131

132 133 134 135
    // If the last build time isn't set before this early return, empty pubspecs will
    // hang on hot reload, as the incremental dill files will never be copied to the
    // device.
    _lastBuildTimestamp = DateTime.now();
136
    if (flutterManifest.isEmpty) {
137
      entries[_assetManifestJson] = DevFSStringContent('{}');
138 139 140
      return 0;
    }

141
    final String assetBasePath = globals.fs.path.dirname(globals.fs.path.absolute(manifestPath));
142

143
    final PackageMap packageMap = PackageMap(packagesPath);
144
    final List<Uri> wildcardDirectories = <Uri>[];
145

146 147 148 149
    // The _assetVariants map contains an entry for each asset listed
    // in the pubspec.yaml file's assets and font and sections. The
    // value of each image asset is a list of resolution-specific "variants",
    // see _AssetDirectoryCache.
150
    final Map<_Asset, List<_Asset>> assetVariants = _parseAssets(
151
      packageMap,
152
      flutterManifest,
153
      wildcardDirectories,
154
      assetBasePath,
155
      excludeDirs: <String>[assetDirPath, getBuildDirectory()],
156 157
    );

158
    if (assetVariants == null) {
159
      return 1;
160
    }
161

162 163 164 165 166
    final List<Map<String, dynamic>> fonts = _parseFonts(
      flutterManifest,
      includeDefaultFonts,
      packageMap,
    );
167

168
    // Add fonts and assets from packages.
169 170 171
    for (String packageName in packageMap.map.keys) {
      final Uri package = packageMap.map[packageName];
      if (package != null && package.scheme == 'file') {
172
        final String packageManifestPath = globals.fs.path.fromUri(package.resolve('../pubspec.yaml'));
173
        final FlutterManifest packageFlutterManifest = FlutterManifest.createFromPath(packageManifestPath);
174
        if (packageFlutterManifest == null) {
175
          continue;
176
        }
177
        // Skip the app itself
178
        if (packageFlutterManifest.appName == flutterManifest.appName) {
179
          continue;
180
        }
181
        final String packageBasePath = globals.fs.path.dirname(packageManifestPath);
182 183 184 185

        final Map<_Asset, List<_Asset>> packageAssets = _parseAssets(
          packageMap,
          packageFlutterManifest,
186
          wildcardDirectories,
187 188 189 190
          packageBasePath,
          packageName: packageName,
        );

191
        if (packageAssets == null) {
192
          return 1;
193
        }
194 195 196 197 198 199 200 201
        assetVariants.addAll(packageAssets);

        fonts.addAll(_parseFonts(
          packageFlutterManifest,
          includeDefaultFonts,
          packageMap,
          packageName: packageName,
        ));
202 203 204
      }
    }

205 206
    // Save the contents of each image, image variant, and font
    // asset in entries.
207
    for (_Asset asset in assetVariants.keys) {
208
      if (!asset.assetFileExists && assetVariants[asset].isEmpty) {
209 210
        globals.printStatus('Error detected in pubspec.yaml:', emphasis: true);
        globals.printError('No file or variants found for $asset.\n');
211 212
        return 1;
      }
213 214 215 216 217 218 219 220 221 222
      // The file name for an asset's "main" entry is whatever appears in
      // the pubspec.yaml file. The main entry's file must always exist for
      // font assets. It need not exist for an image if resolution-specific
      // variant files exist. An image's main entry is treated the same as a
      // "1x" resolution variant and if both exist then the explicit 1x
      // variant is preferred.
      if (asset.assetFileExists) {
        assert(!assetVariants[asset].contains(asset));
        assetVariants[asset].insert(0, asset);
      }
223
      for (_Asset variant in assetVariants[asset]) {
224
        assert(variant.assetFileExists);
225
        entries[variant.entryUri.path] ??= DevFSFileContent(variant.assetFile);
226 227 228
      }
    }

229 230 231 232
    final List<_Asset> materialAssets = <_Asset>[
      if (flutterManifest.usesMaterialDesign && includeDefaultFonts)
        ..._getMaterialAssets(_fontSetMaterial),
    ];
233
    for (_Asset asset in materialAssets) {
234
      assert(asset.assetFileExists);
235
      entries[asset.entryUri.path] ??= DevFSFileContent(asset.assetFile);
236 237
    }

238 239
    // Update wildcard directories we we can detect changes in them.
    for (Uri uri in wildcardDirectories) {
240
      _wildcardDirectories[uri] ??= globals.fs.directory(uri);
241 242
    }

243
    entries[_assetManifestJson] = _createAssetManifest(assetVariants);
244

245
    entries[_fontManifestJson] = DevFSStringContent(json.encode(fonts));
246

247
    // TODO(ianh): Only do the following line if we've changed packages or if our LICENSE file changed
248
    entries[_license] = _obtainLicenses(packageMap, assetBasePath, reportPackages: reportLicensedPackages);
249 250 251 252 253 254

    return 0;
  }
}

class _Asset {
255
  _Asset({ this.baseDir, this.relativeUri, this.entryUri });
256

257
  final String baseDir;
258

259
  /// A platform-independent URL where this asset can be found on disk on the
260 261
  /// host system relative to [baseDir].
  final Uri relativeUri;
262

263
  /// A platform-independent URL representing the entry for the asset manifest.
264
  final Uri entryUri;
265 266

  File get assetFile {
267
    return globals.fs.file(globals.fs.path.join(baseDir, globals.fs.path.fromUri(relativeUri)));
268 269 270 271
  }

  bool get assetFileExists => assetFile.existsSync();

272
  /// The delta between what the entryUri is and the relativeUri (e.g.,
273
  /// packages/flutter_gallery).
274
  Uri get symbolicPrefixUri {
275
    if (entryUri == relativeUri) {
276
      return null;
277
    }
278
    final int index = entryUri.path.indexOf(relativeUri.path);
279
    return index == -1 ? null : Uri(path: entryUri.path.substring(0, index));
280 281 282
  }

  @override
283
  String toString() => 'asset: $entryUri';
284 285 286

  @override
  bool operator ==(dynamic other) {
287
    if (identical(other, this)) {
288
      return true;
289 290
    }
    if (other.runtimeType != runtimeType) {
291
      return false;
292
    }
293 294 295 296
    return other is _Asset
        && other.baseDir == baseDir
        && other.relativeUri == relativeUri
        && other.entryUri == entryUri;
297 298 299 300
  }

  @override
  int get hashCode {
301
    return baseDir.hashCode
302
        ^ relativeUri.hashCode
303
        ^ entryUri.hashCode;
304
  }
305 306 307
}

Map<String, dynamic> _readMaterialFontsManifest() {
308
  final String fontsPath = globals.fs.path.join(globals.fs.path.absolute(Cache.flutterRoot),
309 310
      'packages', 'flutter_tools', 'schema', 'material_fonts.yaml');

311
  return castStringKeyedMap(loadYaml(globals.fs.file(fontsPath).readAsStringSync()));
312 313 314 315 316
}

final Map<String, dynamic> _materialFontsManifest = _readMaterialFontsManifest();

List<Map<String, dynamic>> _getMaterialFonts(String fontSet) {
317
  final List<dynamic> fontsList = _materialFontsManifest[fontSet] as List<dynamic>;
318
  return fontsList?.map<Map<String, dynamic>>(castStringKeyedMap)?.toList();
319 320 321
}

List<_Asset> _getMaterialAssets(String fontSet) {
322
  final List<_Asset> result = <_Asset>[];
323 324

  for (Map<String, dynamic> family in _getMaterialFonts(fontSet)) {
325
    for (Map<dynamic, dynamic> font in family['fonts']) {
326
      final Uri entryUri = globals.fs.path.toUri(font['asset'] as String);
327
      result.add(_Asset(
328
        baseDir: globals.fs.path.join(Cache.flutterRoot, 'bin', 'cache', 'artifacts', 'material_fonts'),
329
        relativeUri: Uri(path: entryUri.pathSegments.last),
330
        entryUri: entryUri,
331 332 333 334 335 336 337 338 339
      ));
    }
  }

  return result;
}

final String _licenseSeparator = '\n' + ('-' * 80) + '\n';

340
/// Returns a DevFSContent representing the license file.
341
DevFSContent _obtainLicenses(
342
  PackageMap packageMap,
343 344
  String assetBase, {
  bool reportPackages,
345
}) {
346 347 348 349 350 351 352 353 354 355 356 357 358
  // Read the LICENSE file from each package in the .packages file, splitting
  // each one into each component license (so that we can de-dupe if possible).
  //
  // Individual licenses inside each LICENSE file should be separated by 80
  // hyphens on their own on a line.
  //
  // If a LICENSE file contains more than one component license, then each
  // component license must start with the names of the packages to which the
  // component license applies, with each package name on its own line, and the
  // list of package names separated from the actual license text by a blank
  // line. (The packages need not match the names of the pub package. For
  // example, a package might itself contain code from multiple third-party
  // sources, and might need to include a license for each one.)
359
  final Map<String, Set<String>> packageLicenses = <String, Set<String>>{};
360
  final Set<String> allPackages = <String>{};
361 362
  for (String packageName in packageMap.map.keys) {
    final Uri package = packageMap.map[packageName];
363 364 365
    if (package == null || package.scheme != 'file') {
      continue;
    }
366
    final File file = globals.fs.file(package.resolve('../LICENSE'));
367 368 369 370 371 372 373 374 375 376 377 378 379
    if (!file.existsSync()) {
      continue;
    }
    final List<String> rawLicenses =
        file.readAsStringSync().split(_licenseSeparator);
    for (String rawLicense in rawLicenses) {
      List<String> packageNames;
      String licenseText;
      if (rawLicenses.length > 1) {
        final int split = rawLicense.indexOf('\n\n');
        if (split >= 0) {
          packageNames = rawLicense.substring(0, split).split('\n');
          licenseText = rawLicense.substring(split + 2);
380 381
        }
      }
382 383 384 385 386 387 388
      if (licenseText == null) {
        packageNames = <String>[packageName];
        licenseText = rawLicense;
      }
      packageLicenses.putIfAbsent(licenseText, () => <String>{})
        ..addAll(packageNames);
      allPackages.addAll(packageNames);
389 390 391
    }
  }

392 393
  if (reportPackages) {
    final List<String> allPackagesList = allPackages.toList()..sort();
394 395
    globals.printStatus('Licenses were found for the following packages:');
    globals.printStatus(allPackagesList.join(', '));
396 397
  }

398
  final List<String> combinedLicensesList = packageLicenses.keys.map<String>(
399
    (String license) {
400
      final List<String> packageNames = packageLicenses[license].toList()
401 402 403 404 405 406 407 408
       ..sort();
      return packageNames.join('\n') + '\n\n' + license;
    }
  ).toList();
  combinedLicensesList.sort();

  final String combinedLicenses = combinedLicensesList.join(_licenseSeparator);

409
  return DevFSStringContent(combinedLicenses);
410 411
}

412 413 414 415
int _byBasename(_Asset a, _Asset b) {
  return a.assetFile.basename.compareTo(b.assetFile.basename);
}

416
DevFSContent _createAssetManifest(Map<_Asset, List<_Asset>> assetVariants) {
417
  final Map<String, List<String>> jsonObject = <String, List<String>>{};
418 419 420 421 422 423 424

  // necessary for making unit tests deterministic
  final List<_Asset> sortedKeys = assetVariants
      .keys.toList()
    ..sort(_byBasename);

  for (_Asset main in sortedKeys) {
425 426 427 428
    jsonObject[main.entryUri.path] = <String>[
      for (_Asset variant in assetVariants[main])
        variant.entryUri.path,
    ];
429
  }
430
  return DevFSStringContent(json.encode(jsonObject));
431 432
}

433
List<Map<String, dynamic>> _parseFonts(
434
  FlutterManifest manifest,
435 436
  bool includeDefaultFonts,
  PackageMap packageMap, {
437
  String packageName,
438
}) {
439 440 441 442 443 444 445 446 447 448 449 450
  return <Map<String, dynamic>>[
    if (manifest.usesMaterialDesign && includeDefaultFonts)
      ..._getMaterialFonts(_ManifestAssetBundle._fontSetMaterial),
    if (packageName == null)
      ...manifest.fontsDescriptor
    else
      ..._createFontsDescriptor(_parsePackageFonts(
        manifest,
        packageName,
        packageMap,
      )),
  ];
451 452 453
}

/// Prefixes family names and asset paths of fonts included from packages with
454 455 456
/// 'packages/<package_name>'
List<Font> _parsePackageFonts(
  FlutterManifest manifest,
457 458 459
  String packageName,
  PackageMap packageMap,
) {
460 461 462 463
  final List<Font> packageFonts = <Font>[];
  for (Font font in manifest.fonts) {
    final List<FontAsset> packageFontAssets = <FontAsset>[];
    for (FontAsset fontAsset in font.fontAssets) {
464 465
      final Uri assetUri = fontAsset.assetUri;
      if (assetUri.pathSegments.first == 'packages' &&
466
          !globals.fs.isFileSync(globals.fs.path.fromUri(packageMap.map[packageName].resolve('../${assetUri.path}')))) {
467
        packageFontAssets.add(FontAsset(
468
          fontAsset.assetUri,
469 470 471
          weight: fontAsset.weight,
          style: fontAsset.style,
        ));
472
      } else {
473
        packageFontAssets.add(FontAsset(
474
          Uri(pathSegments: <String>['packages', packageName, ...assetUri.pathSegments]),
475 476 477
          weight: fontAsset.weight,
          style: fontAsset.style,
        ));
478 479
      }
    }
480
    packageFonts.add(Font('packages/$packageName/${font.familyName}', packageFontAssets));
481
  }
482 483
  return packageFonts;
}
484

485
List<Map<String, dynamic>> _createFontsDescriptor(List<Font> fonts) {
486
  return fonts.map<Map<String, dynamic>>((Font font) => font.descriptor).toList();
487 488
}

489 490 491 492 493 494 495 496 497 498 499
// Given an assets directory like this:
//
// assets/foo
// assets/var1/foo
// assets/var2/foo
// assets/bar
//
// variantsFor('assets/foo') => ['/assets/var1/foo', '/assets/var2/foo']
// variantsFor('assets/bar') => []
class _AssetDirectoryCache {
  _AssetDirectoryCache(Iterable<String> excluded) {
500
    _excluded = excluded.map<String>((String path) => globals.fs.path.absolute(path) + globals.fs.path.separator);
501 502 503 504 505 506
  }

  Iterable<String> _excluded;
  final Map<String, Map<String, List<String>>> _cache = <String, Map<String, List<String>>>{};

  List<String> variantsFor(String assetPath) {
507 508
    final String assetName = globals.fs.path.basename(assetPath);
    final String directory = globals.fs.path.dirname(assetPath);
509

510
    if (!globals.fs.directory(directory).existsSync()) {
511
      return const <String>[];
512
    }
513

514 515
    if (_cache[directory] == null) {
      final List<String> paths = <String>[];
516
      for (FileSystemEntity entity in globals.fs.directory(directory).listSync(recursive: true)) {
517
        final String path = entity.path;
518
        if (globals.fs.isFileSync(path) && !_excluded.any((String exclude) => path.startsWith(exclude))) {
519
          paths.add(path);
520
        }
521 522 523 524
      }

      final Map<String, List<String>> variants = <String, List<String>>{};
      for (String path in paths) {
525 526
        final String variantName = globals.fs.path.basename(path);
        if (directory == globals.fs.path.dirname(path)) {
527
          continue;
528
        }
529 530 531 532 533 534 535 536 537 538
        variants[variantName] ??= <String>[];
        variants[variantName].add(path);
      }
      _cache[directory] = variants;
    }

    return _cache[directory][assetName] ?? const <String>[];
  }
}

539 540
/// Given an assetBase location and a pubspec.yaml Flutter manifest, return a
/// map of assets to asset variants.
541
///
542
/// Returns null on missing assets.
543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563
///
/// Given package: 'test_package' and an assets directory like this:
///
/// assets/foo
/// assets/var1/foo
/// assets/var2/foo
/// assets/bar
///
/// returns
/// {
///   asset: packages/test_package/assets/foo: [
///     asset: packages/test_package/assets/foo,
///     asset: packages/test_package/assets/var1/foo,
///     asset: packages/test_package/assets/var2/foo,
///   ],
///   asset: packages/test_package/assets/bar: [
///     asset: packages/test_package/assets/bar,
///   ],
/// }
///

564 565
Map<_Asset, List<_Asset>> _parseAssets(
  PackageMap packageMap,
566
  FlutterManifest flutterManifest,
567
  List<Uri> wildcardDirectories,
568
  String assetBase, {
569
  List<String> excludeDirs = const <String>[],
570
  String packageName,
571
}) {
572
  final Map<_Asset, List<_Asset>> result = <_Asset, List<_Asset>>{};
573

574
  final _AssetDirectoryCache cache = _AssetDirectoryCache(excludeDirs);
575
  for (Uri assetUri in flutterManifest.assets) {
576
    if (assetUri.toString().endsWith('/')) {
577
      wildcardDirectories.add(assetUri);
578 579 580 581 582 583 584
      _parseAssetsFromFolder(packageMap, flutterManifest, assetBase,
          cache, result, assetUri,
          excludeDirs: excludeDirs, packageName: packageName);
    } else {
      _parseAssetFromFile(packageMap, flutterManifest, assetBase,
          cache, result, assetUri,
          excludeDirs: excludeDirs, packageName: packageName);
585 586 587 588 589 590 591
    }
  }

  // Add assets referenced in the fonts section of the manifest.
  for (Font font in flutterManifest.fonts) {
    for (FontAsset fontAsset in font.fontAssets) {
      final _Asset baseAsset = _resolveAsset(
592 593
        packageMap,
        assetBase,
594
        fontAsset.assetUri,
595 596
        packageName,
      );
597
      if (!baseAsset.assetFileExists) {
598
        globals.printError('Error: unable to locate asset entry in pubspec.yaml: "${fontAsset.assetUri}".');
599
        return null;
600
      }
601

602
      result[baseAsset] = <_Asset>[];
603 604 605 606 607 608
    }
  }

  return result;
}

609 610
void _parseAssetsFromFolder(
  PackageMap packageMap,
611 612 613 614 615
  FlutterManifest flutterManifest,
  String assetBase,
  _AssetDirectoryCache cache,
  Map<_Asset, List<_Asset>> result,
  Uri assetUri, {
616
  List<String> excludeDirs = const <String>[],
617
  String packageName,
618
}) {
619 620
  final String directoryPath = globals.fs.path.join(
      assetBase, assetUri.toFilePath(windows: globals.platform.isWindows));
621

622 623
  if (!globals.fs.directory(directoryPath).existsSync()) {
    globals.printError('Error: unable to find directory entry in pubspec.yaml: $directoryPath');
624 625 626
    return;
  }

627
  final List<FileSystemEntity> lister = globals.fs.directory(directoryPath).listSync();
628 629 630

  for (FileSystemEntity entity in lister) {
    if (entity is File) {
631
      final String relativePath = globals.fs.path.relative(entity.path, from: assetBase);
632

633
      final Uri uri = Uri.file(relativePath, windows: globals.platform.isWindows);
634 635 636 637 638 639 640

      _parseAssetFromFile(packageMap, flutterManifest, assetBase, cache, result,
          uri, packageName: packageName);
    }
  }
}

641 642
void _parseAssetFromFile(
  PackageMap packageMap,
643 644 645 646 647
  FlutterManifest flutterManifest,
  String assetBase,
  _AssetDirectoryCache cache,
  Map<_Asset, List<_Asset>> result,
  Uri assetUri, {
648
  List<String> excludeDirs = const <String>[],
649
  String packageName,
650 651 652 653 654 655 656 657 658
}) {
  final _Asset asset = _resolveAsset(
    packageMap,
    assetBase,
    assetUri,
    packageName,
  );
  final List<_Asset> variants = <_Asset>[];
  for (String path in cache.variantsFor(asset.assetFile.path)) {
659 660
    final String relativePath = globals.fs.path.relative(path, from: asset.baseDir);
    final Uri relativeUri = globals.fs.path.toUri(relativePath);
661 662 663 664 665
    final Uri entryUri = asset.symbolicPrefixUri == null
        ? relativeUri
        : asset.symbolicPrefixUri.resolveUri(relativeUri);

    variants.add(
666
      _Asset(
667 668 669
        baseDir: asset.baseDir,
        entryUri: entryUri,
        relativeUri: relativeUri,
670
      ),
671 672 673 674 675 676
    );
  }

  result[asset] = variants;
}

677 678
_Asset _resolveAsset(
  PackageMap packageMap,
679 680
  String assetsBaseDir,
  Uri assetUri,
681
  String packageName,
682
) {
683 684
  final String assetPath = globals.fs.path.fromUri(assetUri);
  if (assetUri.pathSegments.first == 'packages' && !globals.fs.isFileSync(globals.fs.path.join(assetsBaseDir, assetPath))) {
685
    // The asset is referenced in the pubspec.yaml as
686
    // 'packages/PACKAGE_NAME/PATH/TO/ASSET .
687
    final _Asset packageAsset = _resolvePackageAsset(assetUri, packageMap);
688
    if (packageAsset != null) {
689
      return packageAsset;
690
    }
691
  }
692

693
  return _Asset(
694 695 696
    baseDir: assetsBaseDir,
    entryUri: packageName == null
        ? assetUri // Asset from the current application.
697
        : Uri(pathSegments: <String>['packages', packageName, ...assetUri.pathSegments]), // Asset from, and declared in $packageName.
698 699
    relativeUri: assetUri,
  );
700 701
}

702 703 704 705 706 707
_Asset _resolvePackageAsset(Uri assetUri, PackageMap packageMap) {
  assert(assetUri.pathSegments.first == 'packages');
  if (assetUri.pathSegments.length > 1) {
    final String packageName = assetUri.pathSegments[1];
    final Uri packageUri = packageMap.map[packageName];
    if (packageUri != null && packageUri.scheme == 'file') {
708
      return _Asset(
709
        baseDir: globals.fs.path.fromUri(packageUri),
710
        entryUri: assetUri,
711
        relativeUri: Uri(pathSegments: assetUri.pathSegments.sublist(2)),
712 713
      );
    }
714
  }
715 716
  globals.printStatus('Error detected in pubspec.yaml:', emphasis: true);
  globals.printError('Could not resolve package for asset $assetUri.\n');
717
  return null;
718
}