asset.dart 21.5 KB
Newer Older
1 2 3 4 5 6 7 8 9
// Copyright 2016 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.

import 'dart:async';
import 'dart:convert';

import 'package:yaml/yaml.dart';

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

20 21 22 23 24
const AssetBundleFactory _kManifestFactory = const _ManifestAssetBundleFactory();

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

  static AssetBundleFactory get defaultInstance => _kManifestFactory;
28 29 30 31 32 33 34 35

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

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

36 37
  bool wasBuiltOnce();

38 39 40 41 42
  bool needsBuild({String manifestPath: _ManifestAssetBundle.defaultManifestPath});

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

class _ManifestAssetBundleFactory implements AssetBundleFactory {
  const _ManifestAssetBundleFactory();

  @override
  AssetBundle createBundle() => new _ManifestAssetBundle();
}

class _ManifestAssetBundle implements AssetBundle {
  @override
59
  final Map<String, DevFSContent> entries = <String, DevFSContent>{};
60

61
  static const String defaultManifestPath = 'pubspec.yaml';
62 63 64 65
  static const String _assetManifestJson = 'AssetManifest.json';
  static const String _fontManifestJson = 'FontManifest.json';
  static const String _fontSetMaterial = 'material';
  static const String _license = 'LICENSE';
66

67 68
  DateTime _lastBuildTimestamp;

69
  /// Constructs an [_ManifestAssetBundle] that gathers the set of assets from the
70
  /// pubspec.yaml manifest.
71
  _ManifestAssetBundle();
72

73 74 75
  @override
  bool wasBuiltOnce() => _lastBuildTimestamp != null;

76
  @override
77 78 79 80
  bool needsBuild({String manifestPath: defaultManifestPath}) {
    if (_lastBuildTimestamp == null)
      return true;

81
    final FileStat stat = fs.file(manifestPath).statSync();
82
    if (stat.type == FileSystemEntityType.NOT_FOUND) // ignore: deprecated_member_use
83 84 85 86 87
      return true;

    return stat.modified.isAfter(_lastBuildTimestamp);
  }

88
  @override
89 90
  Future<int> build({
    String manifestPath: defaultManifestPath,
91
    String assetDirPath,
92
    String packagesPath,
93
    bool includeDefaultFonts: true,
94 95
    bool reportLicensedPackages: false
  }) async {
96
    assetDirPath ??= getAssetBuildDirectory();
97
    packagesPath ??= fs.path.absolute(PackageMap.globalPackagesPath);
98
    FlutterManifest flutterManifest;
99
    try {
100
      flutterManifest = await FlutterManifest.createFromPath(manifestPath);
101
    } catch (e) {
102
      printStatus('Error detected in pubspec.yaml:', emphasis: true);
103
      printError('$e');
104 105
      return 1;
    }
106 107 108 109
    if (flutterManifest == null)
      return 1;

    if (flutterManifest.isEmpty) {
110
      entries[_assetManifestJson] = new DevFSStringContent('{}');
111 112 113
      return 0;
    }

114
    final String assetBasePath = fs.path.dirname(fs.path.absolute(manifestPath));
115

116 117
    _lastBuildTimestamp = new DateTime.now();

118
    final PackageMap packageMap = new PackageMap(packagesPath);
119

120 121 122 123
    // 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.
124
    final Map<_Asset, List<_Asset>> assetVariants = _parseAssets(
125
      packageMap,
126
      flutterManifest,
127
      assetBasePath,
128
      excludeDirs: <String>[assetDirPath, getBuildDirectory()]
129 130 131 132 133
    );

    if (assetVariants == null)
      return 1;

134 135 136 137 138
    final List<Map<String, dynamic>> fonts = _parseFonts(
      flutterManifest,
      includeDefaultFonts,
      packageMap,
    );
139

140
    // Add fonts and assets from packages.
141 142 143
    for (String packageName in packageMap.map.keys) {
      final Uri package = packageMap.map[packageName];
      if (package != null && package.scheme == 'file') {
144
        final String packageManifestPath = fs.path.fromUri(package.resolve('../pubspec.yaml'));
145 146
        final FlutterManifest packageFlutterManifest = await FlutterManifest.createFromPath(packageManifestPath);
        if (packageFlutterManifest == null)
147
          continue;
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
        // Skip the app itself
        if (packageFlutterManifest.appName == flutterManifest.appName)
          continue;
        final String packageBasePath = fs.path.dirname(packageManifestPath);

        final Map<_Asset, List<_Asset>> packageAssets = _parseAssets(
          packageMap,
          packageFlutterManifest,
          packageBasePath,
          packageName: packageName,
        );

        if (packageAssets == null)
          return 1;
        assetVariants.addAll(packageAssets);

        fonts.addAll(_parseFonts(
          packageFlutterManifest,
          includeDefaultFonts,
          packageMap,
          packageName: packageName,
        ));
170 171 172
      }
    }

173 174
    // Save the contents of each image, image variant, and font
    // asset in entries.
175
    for (_Asset asset in assetVariants.keys) {
176 177 178 179 180
      if (!asset.assetFileExists && assetVariants[asset].isEmpty) {
        printStatus('Error detected in pubspec.yaml:', emphasis: true);
        printError('No file or variants found for $asset.\n');
        return 1;
      }
181 182 183 184 185 186 187 188 189 190
      // 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);
      }
191
      for (_Asset variant in assetVariants[asset]) {
192
        assert(variant.assetFileExists);
193
        entries[variant.entryUri.path] = new DevFSFileContent(variant.assetFile);
194 195 196
      }
    }

197
    final List<_Asset> materialAssets = <_Asset>[];
198
    if (flutterManifest.usesMaterialDesign && includeDefaultFonts) {
199
      materialAssets.addAll(_getMaterialAssets(_fontSetMaterial));
200 201
    }
    for (_Asset asset in materialAssets) {
202
      assert(asset.assetFileExists);
203
      entries[asset.entryUri.path] = new DevFSFileContent(asset.assetFile);
204 205
    }

206
    entries[_assetManifestJson] = _createAssetManifest(assetVariants);
207

208
    entries[_fontManifestJson] = new DevFSStringContent(json.encode(fonts));
209

210
    // TODO(ianh): Only do the following line if we've changed packages or if our LICENSE file changed
211
    entries[_license] = await _obtainLicenses(packageMap, assetBasePath, reportPackages: reportLicensedPackages);
212 213 214 215 216 217

    return 0;
  }
}

class _Asset {
218
  _Asset({ this.baseDir, this.relativeUri, this.entryUri });
219

220
  final String baseDir;
221

222 223 224
  /// A platform-independent Uri where this asset can be found on disk on the
  /// host system relative to [baseDir].
  final Uri relativeUri;
225

226 227
  /// A platform-independent Uri representing the entry for the asset manifest.
  final Uri entryUri;
228 229

  File get assetFile {
230
    return fs.file(fs.path.join(baseDir, fs.path.fromUri(relativeUri)));
231 232 233 234
  }

  bool get assetFileExists => assetFile.existsSync();

235
  /// The delta between what the entryUri is and the relativeUri (e.g.,
236
  /// packages/flutter_gallery).
237 238
  Uri get symbolicPrefixUri {
    if (entryUri == relativeUri)
239
      return null;
240 241
    final int index = entryUri.path.indexOf(relativeUri.path);
    return index == -1 ? null : new Uri(path: entryUri.path.substring(0, index));
242 243 244
  }

  @override
245
  String toString() => 'asset: $entryUri';
246 247 248 249 250 251 252 253

  @override
  bool operator ==(dynamic other) {
    if (identical(other, this))
      return true;
    if (other.runtimeType != runtimeType)
      return false;
    final _Asset otherAsset = other;
254 255 256
    return otherAsset.baseDir == baseDir
        && otherAsset.relativeUri == relativeUri
        && otherAsset.entryUri == entryUri;
257 258 259 260
  }

  @override
  int get hashCode {
261 262 263
    return baseDir.hashCode
        ^relativeUri.hashCode
        ^ entryUri.hashCode;
264
  }
265 266 267
}

Map<String, dynamic> _readMaterialFontsManifest() {
268
  final String fontsPath = fs.path.join(fs.path.absolute(Cache.flutterRoot),
269 270
      'packages', 'flutter_tools', 'schema', 'material_fonts.yaml');

271
  return loadYaml(fs.file(fontsPath).readAsStringSync());
272 273 274 275 276 277 278 279 280
}

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

List<Map<String, dynamic>> _getMaterialFonts(String fontSet) {
  return _materialFontsManifest[fontSet];
}

List<_Asset> _getMaterialAssets(String fontSet) {
281
  final List<_Asset> result = <_Asset>[];
282 283 284

  for (Map<String, dynamic> family in _getMaterialFonts(fontSet)) {
    for (Map<String, dynamic> font in family['fonts']) {
285
      final Uri entryUri = fs.path.toUri(font['asset']);
286
      result.add(new _Asset(
287 288 289
        baseDir: fs.path.join(Cache.flutterRoot, 'bin', 'cache', 'artifacts', 'material_fonts'),
        relativeUri: new Uri(path: entryUri.pathSegments.last),
        entryUri: entryUri
290 291 292 293 294 295 296 297 298
      ));
    }
  }

  return result;
}

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

299 300
/// Returns a DevFSContent representing the license file.
Future<DevFSContent> _obtainLicenses(
301
  PackageMap packageMap,
302 303
  String assetBase,
  { bool reportPackages }
304
) async {
305 306 307 308 309 310 311 312 313 314 315 316 317
  // 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.)
318
  final Map<String, Set<String>> packageLicenses = <String, Set<String>>{};
319
  final Set<String> allPackages = new Set<String>();
320 321 322
  for (String packageName in packageMap.map.keys) {
    final Uri package = packageMap.map[packageName];
    if (package != null && package.scheme == 'file') {
323
      final File file = fs.file(package.resolve('../LICENSE'));
324 325 326 327 328
      if (file.existsSync()) {
        final List<String> rawLicenses =
            (await file.readAsString()).split(_licenseSeparator);
        for (String rawLicense in rawLicenses) {
          List<String> packageNames;
329
          String licenseText;
330
          if (rawLicenses.length > 1) {
331 332 333 334 335 336 337 338
            final int split = rawLicense.indexOf('\n\n');
            if (split >= 0) {
              packageNames = rawLicense.substring(0, split).split('\n');
              licenseText = rawLicense.substring(split + 2);
            }
          }
          if (licenseText == null) {
            packageNames = <String>[packageName];
339
            licenseText = rawLicense;
340
          }
341
          packageLicenses.putIfAbsent(licenseText, () => new Set<String>())
342
            ..addAll(packageNames);
343
          allPackages.addAll(packageNames);
344 345 346 347 348
        }
      }
    }
  }

349 350 351 352 353 354
  if (reportPackages) {
    final List<String> allPackagesList = allPackages.toList()..sort();
    printStatus('Licenses were found for the following packages:');
    printStatus(allPackagesList.join(', '));
  }

355 356
  final List<String> combinedLicensesList = packageLicenses.keys.map(
    (String license) {
357
      final List<String> packageNames = packageLicenses[license].toList()
358 359 360 361 362 363 364 365
       ..sort();
      return packageNames.join('\n') + '\n\n' + license;
    }
  ).toList();
  combinedLicensesList.sort();

  final String combinedLicenses = combinedLicensesList.join(_licenseSeparator);

366
  return new DevFSStringContent(combinedLicenses);
367 368
}

369 370 371 372
int _byBasename(_Asset a, _Asset b) {
  return a.assetFile.basename.compareTo(b.assetFile.basename);
}

373
DevFSContent _createAssetManifest(Map<_Asset, List<_Asset>> assetVariants) {
374
  final Map<String, List<String>> jsonObject = <String, List<String>>{};
375 376 377 378 379 380 381

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

  for (_Asset main in sortedKeys) {
382
    final List<String> variants = <String>[];
383
    for (_Asset variant in assetVariants[main])
384
      variants.add(variant.entryUri.path);
385
    jsonObject[main.entryUri.path] = variants;
386
  }
387
  return new DevFSStringContent(json.encode(jsonObject));
388 389
}

390
List<Map<String, dynamic>> _parseFonts(
391
  FlutterManifest manifest,
392 393 394 395
  bool includeDefaultFonts,
  PackageMap packageMap, {
  String packageName
}) {
396
  final List<Map<String, dynamic>> fonts = <Map<String, dynamic>>[];
397
  if (manifest.usesMaterialDesign && includeDefaultFonts) {
398
    fonts.addAll(_getMaterialFonts(_ManifestAssetBundle._fontSetMaterial));
399
  }
400 401 402 403 404 405 406 407 408
  if (packageName == null) {
    fonts.addAll(manifest.fontsDescriptor);
  } else {
    fonts.addAll(_createFontsDescriptor(_parsePackageFonts(
      manifest,
      packageName,
      packageMap,
    )));
  }
409 410 411 412
  return fonts;
}

/// Prefixes family names and asset paths of fonts included from packages with
413 414 415
/// 'packages/<package_name>'
List<Font> _parsePackageFonts(
  FlutterManifest manifest,
416 417 418
  String packageName,
  PackageMap packageMap,
) {
419 420 421 422
  final List<Font> packageFonts = <Font>[];
  for (Font font in manifest.fonts) {
    final List<FontAsset> packageFontAssets = <FontAsset>[];
    for (FontAsset fontAsset in font.fontAssets) {
423 424 425
      final Uri assetUri = fontAsset.assetUri;
      if (assetUri.pathSegments.first == 'packages' &&
          !fs.isFileSync(fs.path.fromUri(packageMap.map[packageName].resolve('../${assetUri.path}')))) {
426
        packageFontAssets.add(new FontAsset(
427
          fontAsset.assetUri,
428 429 430
          weight: fontAsset.weight,
          style: fontAsset.style,
        ));
431
      } else {
432
        packageFontAssets.add(new FontAsset(
433
          new Uri(pathSegments: <String>['packages', packageName]..addAll(assetUri.pathSegments)),
434 435 436
          weight: fontAsset.weight,
          style: fontAsset.style,
        ));
437 438
      }
    }
439
    packageFonts.add(new Font('packages/$packageName/${font.familyName}', packageFontAssets));
440
  }
441 442
  return packageFonts;
}
443

444 445
List<Map<String, dynamic>> _createFontsDescriptor(List<Font> fonts) {
  return fonts.map((Font font) => font.descriptor).toList();
446 447
}

448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468
// 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) {
    _excluded = excluded.map<String>((String path) => fs.path.absolute(path) + fs.path.separator);
  }

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

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

469 470 471
    if (!fs.directory(directory).existsSync())
      return const <String>[];

472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494
    if (_cache[directory] == null) {
      final List<String> paths = <String>[];
      for (FileSystemEntity entity in fs.directory(directory).listSync(recursive: true)) {
        final String path = entity.path;
        if (fs.isFileSync(path) && !_excluded.any((String exclude) => path.startsWith(exclude)))
          paths.add(path);
      }

      final Map<String, List<String>> variants = <String, List<String>>{};
      for (String path in paths) {
        final String variantName = fs.path.basename(path);
        if (directory == fs.path.dirname(path))
          continue;
        variants[variantName] ??= <String>[];
        variants[variantName].add(path);
      }
      _cache[directory] = variants;
    }

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

495 496
/// Given an assetBase location and a pubspec.yaml Flutter manifest, return a
/// map of assets to asset variants.
497
///
498
/// Returns null on missing assets.
499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519
///
/// 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,
///   ],
/// }
///

520 521
Map<_Asset, List<_Asset>> _parseAssets(
  PackageMap packageMap,
522
  FlutterManifest flutterManifest,
523
  String assetBase, {
524
  List<String> excludeDirs: const <String>[],
525
  String packageName
526
}) {
527
  final Map<_Asset, List<_Asset>> result = <_Asset, List<_Asset>>{};
528

529
  final _AssetDirectoryCache cache = new _AssetDirectoryCache(excludeDirs);
530
  for (Uri assetUri in flutterManifest.assets) {
531 532 533 534 535 536 537 538
    if (assetUri.toString().endsWith('/')) {
      _parseAssetsFromFolder(packageMap, flutterManifest, assetBase,
          cache, result, assetUri,
          excludeDirs: excludeDirs, packageName: packageName);
    } else {
      _parseAssetFromFile(packageMap, flutterManifest, assetBase,
          cache, result, assetUri,
          excludeDirs: excludeDirs, packageName: packageName);
539 540 541 542 543 544 545
    }
  }

  // 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(
546 547
        packageMap,
        assetBase,
548
        fontAsset.assetUri,
549 550
        packageName,
      );
551
      if (!baseAsset.assetFileExists) {
552
        printError('Error: unable to locate asset entry in pubspec.yaml: "${fontAsset.assetUri}".');
553
        return null;
554
      }
555

556
      result[baseAsset] = <_Asset>[];
557 558 559 560 561 562
    }
  }

  return result;
}

563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628
void _parseAssetsFromFolder(PackageMap packageMap,
  FlutterManifest flutterManifest,
  String assetBase,
  _AssetDirectoryCache cache,
  Map<_Asset, List<_Asset>> result,
  Uri assetUri, {
  List<String> excludeDirs: const <String>[],
  String packageName
}) {
  final String directoryPath = fs.path.join(
      assetBase, assetUri.toFilePath(windows: platform.isWindows));

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

  final List<FileSystemEntity> lister = fs.directory(directoryPath).listSync();

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

      final Uri uri = new Uri.file(relativePath, windows: platform.isWindows);

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

void _parseAssetFromFile(PackageMap packageMap,
  FlutterManifest flutterManifest,
  String assetBase,
  _AssetDirectoryCache cache,
  Map<_Asset, List<_Asset>> result,
  Uri assetUri, {
  List<String> excludeDirs: const <String>[],
  String packageName
}) {
  final _Asset asset = _resolveAsset(
    packageMap,
    assetBase,
    assetUri,
    packageName,
  );
  final List<_Asset> variants = <_Asset>[];
  for (String path in cache.variantsFor(asset.assetFile.path)) {
    final String relativePath = fs.path.relative(path, from: asset.baseDir);
    final Uri relativeUri = fs.path.toUri(relativePath);
    final Uri entryUri = asset.symbolicPrefixUri == null
        ? relativeUri
        : asset.symbolicPrefixUri.resolveUri(relativeUri);

    variants.add(
      new _Asset(
        baseDir: asset.baseDir,
        entryUri: entryUri,
        relativeUri: relativeUri,
        )
    );
  }

  result[asset] = variants;
}

629 630
_Asset _resolveAsset(
  PackageMap packageMap,
631 632
  String assetsBaseDir,
  Uri assetUri,
633
  String packageName,
634
) {
635 636
  final String assetPath = fs.path.fromUri(assetUri);
  if (assetUri.pathSegments.first == 'packages' && !fs.isFileSync(fs.path.join(assetsBaseDir, assetPath))) {
637
    // The asset is referenced in the pubspec.yaml as
638
    // 'packages/PACKAGE_NAME/PATH/TO/ASSET .
639
    final _Asset packageAsset = _resolvePackageAsset(assetUri, packageMap);
640 641 642
    if (packageAsset != null)
      return packageAsset;
  }
643

644 645 646 647 648 649 650
  return new _Asset(
    baseDir: assetsBaseDir,
    entryUri: packageName == null
        ? assetUri // Asset from the current application.
        : new Uri(pathSegments: <String>['packages', packageName]..addAll(assetUri.pathSegments)), // Asset from, and declared in $packageName.
    relativeUri: assetUri,
  );
651 652
}

653 654 655 656 657 658
_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') {
659
      return new _Asset(
660 661 662
        baseDir: fs.path.fromUri(packageUri),
        entryUri: assetUri,
        relativeUri: new Uri(pathSegments: assetUri.pathSegments.sublist(2)),
663 664
      );
    }
665 666
  }
  printStatus('Error detected in pubspec.yaml:', emphasis: true);
667
  printError('Could not resolve package for asset $assetUri.\n');
668
  return null;
669
}