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

5 6
import 'dart:typed_data';

7
import 'package:meta/meta.dart';
8
import 'package:package_config/package_config.dart';
9
import 'package:standard_message_codec/standard_message_codec.dart';
10

11
import 'base/context.dart';
12
import 'base/deferred_component.dart';
13
import 'base/file_system.dart';
14
import 'base/logger.dart';
15
import 'base/platform.dart';
16
import 'build_info.dart';
17
import 'cache.dart';
18
import 'convert.dart';
19
import 'dart/package_map.dart';
20
import 'devfs.dart';
21
import 'flutter_manifest.dart';
22
import 'license_collector.dart';
23
import 'project.dart';
24

25 26
const String defaultManifestPath = 'pubspec.yaml';

27 28
const String kFontManifestJson = 'FontManifest.json';

29 30 31
// Should match '2x', '/1x', '1.5x', etc.
final RegExp _assetVariantDirectoryRegExp = RegExp(r'/?(\d+(\.\d*)?)x$');

32 33 34
/// The effect of adding `uses-material-design: true` to the pubspec is to insert
/// the following snippet into the asset manifest:
///
35
/// ```yaml
36 37 38 39
/// material:
///   - family: MaterialIcons
///     fonts:
///       - asset: fonts/MaterialIcons-Regular.otf
40
/// ```
41 42 43
const List<Map<String, Object>> kMaterialFonts = <Map<String, Object>>[
  <String, Object>{
    'family': 'MaterialIcons',
44 45
    'fonts': <Map<String, String>>[
      <String, String>{
46 47 48 49 50 51
        'asset': 'fonts/MaterialIcons-Regular.otf',
      },
    ],
  },
];

52 53 54 55
const List<String> kMaterialShaders = <String>[
  'shaders/ink_sparkle.frag',
];

56 57 58
/// Injected factory class for spawning [AssetBundle] instances.
abstract class AssetBundleFactory {
  /// The singleton instance, pulled from the [AppContext].
59
  static AssetBundleFactory get instance => context.get<AssetBundleFactory>()!;
60

61
  static AssetBundleFactory defaultInstance({
62 63 64
    required Logger logger,
    required FileSystem fileSystem,
    required Platform platform,
65 66
    bool splitDeferredAssets = false,
  }) => _ManifestAssetBundleFactory(logger: logger, fileSystem: fileSystem, platform: platform, splitDeferredAssets: splitDeferredAssets);
67 68 69 70 71

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

72 73 74 75
enum AssetKind {
  regular,
  font,
  shader,
76
  model,
77 78
}

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

82 83
  Map<String, AssetKind> get entryKinds;

84 85 86 87
  /// The files that were specified under the deferred components assets sections
  /// in pubspec.
  Map<String, Map<String, DevFSContent>> get deferredComponentsEntries;

88 89 90 91
  /// Additional files that this bundle depends on that are not included in the
  /// output result.
  List<File> get additionalDependencies;

92 93 94
  /// Input files used to build this asset bundle.
  List<File> get inputFiles;

95 96
  bool wasBuiltOnce();

97
  bool needsBuild({ String manifestPath = defaultManifestPath });
98 99 100

  /// Returns 0 for success; non-zero for failure.
  Future<int> build({
101
    String manifestPath = defaultManifestPath,
102
    required String packagesPath,
103
    bool deferredComponentsEnabled = false,
104
    TargetPlatform? targetPlatform,
105 106 107 108
  });
}

class _ManifestAssetBundleFactory implements AssetBundleFactory {
109
  _ManifestAssetBundleFactory({
110 111 112
    required Logger logger,
    required FileSystem fileSystem,
    required Platform platform,
113
    bool splitDeferredAssets = false,
114
  }) : _logger = logger,
115
       _fileSystem = fileSystem,
116 117
       _platform = platform,
       _splitDeferredAssets = splitDeferredAssets;
118 119 120

  final Logger _logger;
  final FileSystem _fileSystem;
121
  final Platform _platform;
122
  final bool _splitDeferredAssets;
123 124

  @override
125
  AssetBundle createBundle() => ManifestAssetBundle(logger: _logger, fileSystem: _fileSystem, platform: _platform, splitDeferredAssets: _splitDeferredAssets);
126 127
}

128
/// An asset bundle based on a pubspec.yaml file.
129 130
class ManifestAssetBundle implements AssetBundle {
  /// Constructs an [ManifestAssetBundle] that gathers the set of assets from the
131
  /// pubspec.yaml manifest.
132
  ManifestAssetBundle({
133 134 135
    required Logger logger,
    required FileSystem fileSystem,
    required Platform platform,
136
    bool splitDeferredAssets = false,
137 138
  }) : _logger = logger,
       _fileSystem = fileSystem,
139
       _platform = platform,
140
       _splitDeferredAssets = splitDeferredAssets,
141 142 143 144 145
       _licenseCollector = LicenseCollector(fileSystem: fileSystem);

  final Logger _logger;
  final FileSystem _fileSystem;
  final LicenseCollector _licenseCollector;
146
  final Platform _platform;
147
  final bool _splitDeferredAssets;
148

149
  @override
150
  final Map<String, DevFSContent> entries = <String, DevFSContent>{};
151

152 153 154
  @override
  final Map<String, AssetKind> entryKinds = <String, AssetKind>{};

155 156 157
  @override
  final Map<String, Map<String, DevFSContent>> deferredComponentsEntries = <String, Map<String, DevFSContent>>{};

158 159 160
  @override
  final List<File> inputFiles = <File>[];

161
  // If an asset corresponds to a wildcard directory, then it may have been
162 163
  // updated without changes to the manifest. These are only tracked for
  // the current project.
164 165
  final Map<Uri, Directory> _wildcardDirectories = <Uri, Directory>{};

166
  DateTime? _lastBuildTimestamp;
167

168 169 170 171 172
  // We assume the main asset is designed for a device pixel ratio of 1.0.
  static const double _defaultResolution = 1.0;
  static const String _kAssetManifestJsonFilename = 'AssetManifest.json';
  static const String _kAssetManifestBinFilename = 'AssetManifest.bin';

173
  static const String _kNoticeFile = 'NOTICES';
174 175 176 177 178 179 180
  // Comically, this can't be name with the more common .gz file extension
  // because when it's part of an AAR and brought into another APK via gradle,
  // gradle individually traverses all the files of the AAR and unzips .gz
  // files (b/37117906). A less common .Z extension still describes how the
  // file is formatted if users want to manually inspect the application
  // bundle and is recognized by default file handlers on OS such as macOS.˚
  static const String _kNoticeZippedFile = 'NOTICES.Z';
181

182 183 184
  @override
  bool wasBuiltOnce() => _lastBuildTimestamp != null;

185
  @override
186
  bool needsBuild({ String manifestPath = defaultManifestPath }) {
187
    final DateTime? lastBuildTimestamp = _lastBuildTimestamp;
188
    if (lastBuildTimestamp == null) {
189
      return true;
190
    }
191

192
    final FileStat stat = _fileSystem.file(manifestPath).statSync();
193
    if (stat.type == FileSystemEntityType.notFound) {
194
      return true;
195
    }
196

197
    for (final Directory directory in _wildcardDirectories.values) {
198 199
      if (!directory.existsSync()) {
        return true; // directory was deleted.
200
      }
201
      for (final File file in directory.listSync().whereType<File>()) {
202
        final DateTime dateTime = file.statSync().modified;
203
        if (dateTime.isAfter(lastBuildTimestamp)) {
204 205
          return true;
        }
206 207 208
      }
    }

209
    return stat.modified.isAfter(lastBuildTimestamp);
210 211
  }

212
  @override
213
  Future<int> build({
214
    String manifestPath = defaultManifestPath,
215
    FlutterProject? flutterProject,
216
    required String packagesPath,
217
    bool deferredComponentsEnabled = false,
218
    TargetPlatform? targetPlatform,
219
  }) async {
220

221
    if (flutterProject == null) {
222 223 224 225 226 227 228
      try {
        flutterProject = FlutterProject.fromDirectory(_fileSystem.file(manifestPath).parent);
      } on Exception catch (e) {
        _logger.printStatus('Error detected in pubspec.yaml:', emphasis: true);
        _logger.printError('$e');
        return 1;
      }
229
    }
230

231
    final FlutterManifest flutterManifest = flutterProject.manifest;
232 233 234 235
    // 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();
236
    if (flutterManifest.isEmpty) {
237 238 239 240 241 242 243 244 245
      entries[_kAssetManifestJsonFilename] = DevFSStringContent('{}');
      entryKinds[_kAssetManifestJsonFilename] = AssetKind.regular;
      entries[_kAssetManifestJsonFilename] = DevFSStringContent('{}');
      entryKinds[_kAssetManifestJsonFilename] = AssetKind.regular;
      final ByteData emptyAssetManifest =
        const StandardMessageCodec().encodeMessage(<dynamic, dynamic>{})!;
      entries[_kAssetManifestBinFilename] =
        DevFSByteContent(emptyAssetManifest.buffer.asUint8List(0, emptyAssetManifest.lengthInBytes));
      entryKinds[_kAssetManifestBinFilename] = AssetKind.regular;
246 247 248
      return 0;
    }

249
    final String assetBasePath = _fileSystem.path.dirname(_fileSystem.path.absolute(manifestPath));
250 251
    final File packageConfigFile = _fileSystem.file(packagesPath);
    inputFiles.add(packageConfigFile);
252
    final PackageConfig packageConfig = await loadPackageConfigWithLogging(
253
      packageConfigFile,
254
      logger: _logger,
255
    );
256
    final List<Uri> wildcardDirectories = <Uri>[];
257

258
    // The _assetVariants map contains an entry for each asset listed
259
    // in the pubspec.yaml file's assets and font sections. The
260 261
    // value of each image asset is a list of resolution-specific "variants",
    // see _AssetDirectoryCache.
262
    final Map<_Asset, List<_Asset>>? assetVariants = _parseAssets(
263
      packageConfig,
264
      flutterManifest,
265
      wildcardDirectories,
266
      assetBasePath,
267
      targetPlatform,
268 269
    );

270
    if (assetVariants == null) {
271
      return 1;
272
    }
273

274
    // Parse assets for deferred components.
275 276 277 278 279 280 281 282 283 284
    final Map<String, Map<_Asset, List<_Asset>>> deferredComponentsAssetVariants = _parseDeferredComponentsAssets(
      flutterManifest,
      packageConfig,
      assetBasePath,
      wildcardDirectories,
      flutterProject.directory,
    );
    if (!_splitDeferredAssets || !deferredComponentsEnabled) {
      // Include the assets in the regular set of assets if not using deferred
      // components.
285
      deferredComponentsAssetVariants.values.forEach(assetVariants.addAll);
286 287 288 289
      deferredComponentsAssetVariants.clear();
      deferredComponentsEntries.clear();
    }

290
    final bool includesMaterialFonts = flutterManifest.usesMaterialDesign;
291
    final List<Map<String, Object?>> fonts = _parseFonts(
292
      flutterManifest,
293
      packageConfig,
294
      primary: true,
295
    );
296

297 298
    // Add fonts, assets, and licenses from packages.
    final Map<String, List<File>> additionalLicenseFiles = <String, List<File>>{};
299 300
    for (final Package package in packageConfig.packages) {
      final Uri packageUri = package.packageUriRoot;
301
      if (packageUri.scheme == 'file') {
302
        final String packageManifestPath = _fileSystem.path.fromUri(packageUri.resolve('../pubspec.yaml'));
303
        inputFiles.add(_fileSystem.file(packageManifestPath));
304
        final FlutterManifest? packageFlutterManifest = FlutterManifest.createFromPath(
305
          packageManifestPath,
306 307
          logger: _logger,
          fileSystem: _fileSystem,
308
        );
309
        if (packageFlutterManifest == null) {
310
          continue;
311
        }
312 313 314 315 316 317 318 319
        // Collect any additional licenses from each package.
        final List<File> licenseFiles = <File>[];
        for (final String relativeLicensePath in packageFlutterManifest.additionalLicenses) {
          final String absoluteLicensePath = _fileSystem.path.fromUri(package.root.resolve(relativeLicensePath));
          licenseFiles.add(_fileSystem.file(absoluteLicensePath).absolute);
        }
        additionalLicenseFiles[packageFlutterManifest.appName] = licenseFiles;

320
        // Skip the app itself
321
        if (packageFlutterManifest.appName == flutterManifest.appName) {
322
          continue;
323
        }
324
        final String packageBasePath = _fileSystem.path.dirname(packageManifestPath);
325

326
        final Map<_Asset, List<_Asset>>? packageAssets = _parseAssets(
327
          packageConfig,
328
          packageFlutterManifest,
329 330
          // Do not track wildcard directories for dependencies.
          <Uri>[],
331
          packageBasePath,
332
          targetPlatform,
333
          packageName: package.name,
334
          attributedPackage: package,
335 336
        );

337
        if (packageAssets == null) {
338
          return 1;
339
        }
340
        assetVariants.addAll(packageAssets);
341
        if (!includesMaterialFonts && packageFlutterManifest.usesMaterialDesign) {
342
          _logger.printError(
343 344 345 346 347 348
            'package:${package.name} has `uses-material-design: true` set but '
            'the primary pubspec contains `uses-material-design: false`. '
            'If the application needs material icons, then `uses-material-design` '
            ' must be set to true.'
          );
        }
349 350
        fonts.addAll(_parseFonts(
          packageFlutterManifest,
351 352
          packageConfig,
          packageName: package.name,
353
          primary: false,
354
        ));
355 356 357
      }
    }

358 359
    // Save the contents of each image, image variant, and font
    // asset in entries.
360
    for (final _Asset asset in assetVariants.keys) {
361
      final File assetFile = asset.lookupAssetFile(_fileSystem);
362 363
      final List<_Asset> variants = assetVariants[asset]!;
      if (!assetFile.existsSync() && variants.isEmpty) {
364 365
        _logger.printStatus('Error detected in pubspec.yaml:', emphasis: true);
        _logger.printError('No file or variants found for $asset.\n');
366
        if (asset.package != null) {
367
          _logger.printError('This asset was included from package ${asset.package?.name}.');
368
        }
369 370
        return 1;
      }
371 372 373 374 375 376
      // 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.
377
      if (assetFile.existsSync() && !variants.contains(asset)) {
378
        variants.insert(0, asset);
379
      }
380
      for (final _Asset variant in variants) {
381
        final File variantFile = variant.lookupAssetFile(_fileSystem);
382
        inputFiles.add(variantFile);
383 384
        assert(variantFile.existsSync());
        entries[variant.entryUri.path] ??= DevFSFileContent(variantFile);
385
        entryKinds[variant.entryUri.path] ??= variant.assetKind;
386 387
      }
    }
388 389
    // Save the contents of each deferred component image, image variant, and font
    // asset in deferredComponentsEntries.
390 391 392 393 394 395 396 397 398 399
    for (final String componentName in deferredComponentsAssetVariants.keys) {
      deferredComponentsEntries[componentName] = <String, DevFSContent>{};
      final Map<_Asset, List<_Asset>> assetsMap = deferredComponentsAssetVariants[componentName]!;
      for (final _Asset asset in assetsMap.keys) {
        final File assetFile = asset.lookupAssetFile(_fileSystem);
        if (!assetFile.existsSync() && assetsMap[asset]!.isEmpty) {
          _logger.printStatus('Error detected in pubspec.yaml:', emphasis: true);
          _logger.printError('No file or variants found for $asset.\n');
          if (asset.package != null) {
            _logger.printError('This asset was included from package ${asset.package?.name}.');
400
          }
401 402 403 404 405 406 407 408 409 410 411 412 413 414 415
          return 1;
        }
        // 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 (assetFile.existsSync() && !assetsMap[asset]!.contains(asset)) {
          assetsMap[asset]!.insert(0, asset);
        }
        for (final _Asset variant in assetsMap[asset]!) {
          final File variantFile = variant.lookupAssetFile(_fileSystem);
          assert(variantFile.existsSync());
          deferredComponentsEntries[componentName]![variant.entryUri.path] ??= DevFSFileContent(variantFile);
416 417 418
        }
      }
    }
419
    final List<_Asset> materialAssets = <_Asset>[
420
      if (flutterManifest.usesMaterialDesign)
421
        ..._getMaterialFonts(),
422
      // For all platforms, include the shaders unconditionally. They are
423 424
      // small, and whether they're used is determined only by the app source
      // code and not by the Flutter manifest.
425
      ..._getMaterialShaders(),
426
    ];
427
    for (final _Asset asset in materialAssets) {
428
      final File assetFile = asset.lookupAssetFile(_fileSystem);
429
      assert(assetFile.existsSync(), 'Missing ${assetFile.path}');
430
      entries[asset.entryUri.path] ??= DevFSFileContent(assetFile);
431
      entryKinds[asset.entryUri.path] ??= asset.assetKind;
432 433
    }

434
    // Update wildcard directories we can detect changes in them.
435
    for (final Uri uri in wildcardDirectories) {
436
      _wildcardDirectories[uri] ??= _fileSystem.directory(uri);
437 438
    }

439 440 441 442
    final Map<String, List<String>> assetManifest =
      _createAssetManifest(assetVariants, deferredComponentsAssetVariants);
    final DevFSStringContent assetManifestJson = DevFSStringContent(json.encode(assetManifest));
    final DevFSByteContent assetManifestBinary = _createAssetManifestBinary(assetManifest);
443
    final DevFSStringContent fontManifest = DevFSStringContent(json.encode(fonts));
444 445 446 447 448 449
    final LicenseResult licenseResult = _licenseCollector.obtainLicenses(packageConfig, additionalLicenseFiles);
    if (licenseResult.errorMessages.isNotEmpty) {
      licenseResult.errorMessages.forEach(_logger.printError);
      return 1;
    }

450
    additionalDependencies = licenseResult.dependencies;
451
    inputFiles.addAll(additionalDependencies);
452

453 454 455 456
    if (wildcardDirectories.isNotEmpty) {
      // Force the depfile to contain missing files so that Gradle does not skip
      // the task. Wildcard directories are not compatible with full incremental
      // builds. For more context see https://github.com/flutter/flutter/issues/56466 .
457
      _logger.printTrace(
458 459 460 461 462
        'Manifest contained wildcard assets. Inserting missing file into '
        'build graph to force rerun. for more information see #56466.'
      );
      final int suffix = Object().hashCode;
      additionalDependencies.add(
463
        _fileSystem.file('DOES_NOT_EXIST_RERUN_FOR_WILDCARD$suffix').absolute);
464 465
    }

466 467
    _setIfChanged(_kAssetManifestJsonFilename, assetManifestJson, AssetKind.regular);
    _setIfChanged(_kAssetManifestBinFilename, assetManifestBinary, AssetKind.regular);
468
    _setIfChanged(kFontManifestJson, fontManifest, AssetKind.regular);
469
    _setLicenseIfChanged(licenseResult.combinedLicenses, targetPlatform);
470 471
    return 0;
  }
472 473 474

  @override
  List<File> additionalDependencies = <File>[];
475 476 477 478 479 480
  void _setIfChanged(String key, DevFSContent content, AssetKind assetKind) {
    final DevFSContent? oldContent = entries[key];
    // In the case that the content is unchanged, we want to avoid an overwrite
    // as the isModified property may be reset to true,
    if (oldContent is DevFSByteContent && content is DevFSByteContent &&
        _compareIntLists(oldContent.bytes, content.bytes)) {
481
      return;
482
    }
483 484 485 486 487 488 489 490

    entries[key] = content;
    entryKinds[key] = assetKind;
  }

  static bool _compareIntLists(List<int> o1, List<int> o2) {
    if (o1.length != o2.length) {
      return false;
491
    }
492 493 494 495 496 497 498 499

    for (int index = 0; index < o1.length; index++) {
      if (o1[index] != o2[index]) {
        return false;
      }
    }

    return true;
500
  }
501

502 503
  void _setLicenseIfChanged(
    String combinedLicenses,
504
    TargetPlatform? targetPlatform,
505 506 507 508 509
  ) {
    // On the web, don't compress the NOTICES file since the client doesn't have
    // dart:io to decompress it. So use the standard _setIfChanged to check if
    // the strings still match.
    if (targetPlatform == TargetPlatform.web_javascript) {
510
      _setIfChanged(_kNoticeFile, DevFSStringContent(combinedLicenses), AssetKind.regular);
511 512 513 514 515 516 517 518
      return;
    }

    // On other platforms, let the NOTICES file be compressed. But use a
    // specialized DevFSStringCompressingBytesContent class to compare
    // the uncompressed strings to not incur decompression/decoding while making
    // the comparison.
    if (!entries.containsKey(_kNoticeZippedFile) ||
519
        (entries[_kNoticeZippedFile] as DevFSStringCompressingBytesContent?)
520
            ?.equals(combinedLicenses) != true) {
521 522 523 524 525 526 527
      entries[_kNoticeZippedFile] = DevFSStringCompressingBytesContent(
        combinedLicenses,
        // A zlib dictionary is a hinting string sequence with the most
        // likely string occurrences at the end. This ends up just being
        // common English words with domain specific words like copyright.
        hintString: 'copyrightsoftwaretothisinandorofthe',
      );
528
      entryKinds[_kNoticeZippedFile] = AssetKind.regular;
529 530 531
    }
  }

532
  List<_Asset> _getMaterialFonts() {
533 534
    final List<_Asset> result = <_Asset>[];
    for (final Map<String, Object> family in kMaterialFonts) {
535
      final Object? fonts = family['fonts'];
536 537 538 539
      if (fonts == null) {
        continue;
      }
      for (final Map<String, Object> font in fonts as List<Map<String, String>>) {
540
        final String? asset = font['asset'] as String?;
541 542 543 544
        if (asset == null) {
          continue;
        }
        final Uri entryUri = _fileSystem.path.toUri(asset);
545
        result.add(_Asset(
546 547 548 549
          baseDir: _fileSystem.path.join(
            Cache.flutterRoot!,
            'bin', 'cache', 'artifacts', 'material_fonts',
          ),
550 551 552
          relativeUri: Uri(path: entryUri.pathSegments.last),
          entryUri: entryUri,
          package: null,
553
          assetKind: AssetKind.font,
554 555 556 557 558 559 560
        ));
      }
    }

    return result;
  }

561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580
  List<_Asset> _getMaterialShaders() {
    final String shaderPath = _fileSystem.path.join(
      Cache.flutterRoot!,
      'packages', 'flutter', 'lib', 'src', 'material', 'shaders',
    );
    // This file will exist in a real invocation unless the git checkout is
    // corrupted somehow, but unit tests generally don't create this file
    // in their mock file systems. Leaving it out in those cases is harmless.
    if (!_fileSystem.directory(shaderPath).existsSync()) {
      return <_Asset>[];
    }

    final List<_Asset> result = <_Asset>[];
    for (final String shader in kMaterialShaders) {
      final Uri entryUri = _fileSystem.path.toUri(shader);
      result.add(_Asset(
        baseDir: shaderPath,
        relativeUri: Uri(path: entryUri.pathSegments.last),
        entryUri: entryUri,
        package: null,
581
        assetKind: AssetKind.shader,
582 583 584 585 586 587
      ));
    }

    return result;
  }

588
  List<Map<String, Object?>> _parseFonts(
589 590
    FlutterManifest manifest,
    PackageConfig packageConfig, {
591 592
    String? packageName,
    required bool primary,
593
  }) {
594
    return <Map<String, Object?>>[
595
      if (primary && manifest.usesMaterialDesign)
596 597 598 599 600 601 602 603 604 605 606
        ...kMaterialFonts,
      if (packageName == null)
        ...manifest.fontsDescriptor
      else
        for (Font font in _parsePackageFonts(
          manifest,
          packageName,
          packageConfig,
        )) font.descriptor,
    ];
  }
607

608 609 610 611 612 613 614 615
  Map<String, Map<_Asset, List<_Asset>>> _parseDeferredComponentsAssets(
    FlutterManifest flutterManifest,
    PackageConfig packageConfig,
    String assetBasePath,
    List<Uri> wildcardDirectories,
    Directory projectDirectory, {
    List<String> excludeDirs = const <String>[],
  }) {
616
    final List<DeferredComponent>? components = flutterManifest.deferredComponents;
617 618 619 620 621 622
    final Map<String, Map<_Asset, List<_Asset>>> deferredComponentsAssetVariants = <String, Map<_Asset, List<_Asset>>>{};
    if (components == null) {
      return deferredComponentsAssetVariants;
    }
    for (final DeferredComponent component in components) {
      deferredComponentsAssetVariants[component.name] = <_Asset, List<_Asset>>{};
623
      final _AssetDirectoryCache cache = _AssetDirectoryCache(_fileSystem);
624 625 626 627 628 629 630 631
      for (final Uri assetUri in component.assets) {
        if (assetUri.path.endsWith('/')) {
          wildcardDirectories.add(assetUri);
          _parseAssetsFromFolder(
            packageConfig,
            flutterManifest,
            assetBasePath,
            cache,
632
            deferredComponentsAssetVariants[component.name]!,
633 634 635 636 637 638 639 640
            assetUri,
          );
        } else {
          _parseAssetFromFile(
            packageConfig,
            flutterManifest,
            assetBasePath,
            cache,
641
            deferredComponentsAssetVariants[component.name]!,
642 643 644 645 646 647 648 649 650
            assetUri,
            excludeDirs: excludeDirs,
          );
        }
      }
    }
    return deferredComponentsAssetVariants;
  }

651
  Map<String, List<String>> _createAssetManifest(
652 653 654
    Map<_Asset, List<_Asset>> assetVariants,
    Map<String, Map<_Asset, List<_Asset>>> deferredComponentsAssetVariants
  ) {
655 656
    final Map<String, List<String>> manifest = <String, List<String>>{};
    final Map<_Asset, List<String>> entries = <_Asset, List<String>>{};
657
    assetVariants.forEach((_Asset main, List<_Asset> variants) {
658
      entries[main] = <String>[
659
        for (final _Asset variant in variants)
660 661
          variant.entryUri.path,
      ];
662
    });
663 664 665 666 667 668 669
    for (final Map<_Asset, List<_Asset>> componentAssets in deferredComponentsAssetVariants.values) {
      componentAssets.forEach((_Asset main, List<_Asset> variants) {
        entries[main] = <String>[
          for (final _Asset variant in variants)
            variant.entryUri.path,
        ];
      });
670
    }
671
    final List<_Asset> sortedKeys = entries.keys.toList()
672 673
        ..sort((_Asset left, _Asset right) => left.entryUri.path.compareTo(right.entryUri.path));
    for (final _Asset main in sortedKeys) {
674
      final String decodedEntryPath = Uri.decodeFull(main.entryUri.path);
675
      final List<String> rawEntryVariantsPaths = entries[main]!;
676 677 678
      final List<String> decodedEntryVariantPaths = rawEntryVariantsPaths
        .map((String value) => Uri.decodeFull(value))
        .toList();
679
      manifest[decodedEntryPath] = decodedEntryVariantPaths;
680
    }
681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726
    return manifest;
  }

  // Matches path-like strings ending in a number followed by an 'x'.
  // Example matches include "assets/animals/2.0x", "plants/3x", and "2.7x".
  static final RegExp _extractPixelRatioFromKeyRegExp = RegExp(r'/?(\d+(\.\d*)?)x$');

  DevFSByteContent _createAssetManifestBinary(
    Map<String, List<String>> assetManifest
  ) {
    double parseScale(String key) {
      final Uri assetUri = Uri.parse(key);
      String directoryPath = '';
      if (assetUri.pathSegments.length > 1) {
        directoryPath = assetUri.pathSegments[assetUri.pathSegments.length - 2];
      }

      final Match? match = _extractPixelRatioFromKeyRegExp.firstMatch(directoryPath);
      if (match != null && match.groupCount > 0) {
        return double.parse(match.group(1)!);
      }
      return _defaultResolution;
    }

    final Map<String, dynamic> result = <String, dynamic>{};

    for (final MapEntry<String, dynamic> manifestEntry in assetManifest.entries) {
      final List<dynamic> resultVariants = <dynamic>[];
      final List<String> entries = (manifestEntry.value as List<dynamic>).cast<String>();
      for (final String variant in entries) {
        if (variant == manifestEntry.key) {
          // With the newer binary format, don't include the main asset in it's
          // list of variants. This reduces parsing time at runtime.
          continue;
        }
        final Map<String, dynamic> resultVariant = <String, dynamic>{};
        final double variantDevicePixelRatio = parseScale(variant);
        resultVariant['asset'] = variant;
        resultVariant['dpr'] = variantDevicePixelRatio;
        resultVariants.add(resultVariant);
      }
      result[manifestEntry.key] = resultVariants;
    }

    final ByteData message = const StandardMessageCodec().encodeMessage(result)!;
    return DevFSByteContent(message.buffer.asUint8List(0, message.lengthInBytes));
727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742
  }

  /// Prefixes family names and asset paths of fonts included from packages with
  /// 'packages/<package_name>'
  List<Font> _parsePackageFonts(
    FlutterManifest manifest,
    String packageName,
    PackageConfig packageConfig,
  ) {
    final List<Font> packageFonts = <Font>[];
    for (final Font font in manifest.fonts) {
      final List<FontAsset> packageFontAssets = <FontAsset>[];
      for (final FontAsset fontAsset in font.fontAssets) {
        final Uri assetUri = fontAsset.assetUri;
        if (assetUri.pathSegments.first == 'packages' &&
            !_fileSystem.isFileSync(_fileSystem.path.fromUri(
743
              packageConfig[packageName]?.packageUriRoot.resolve('../${assetUri.path}')))) {
744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786
          packageFontAssets.add(FontAsset(
            fontAsset.assetUri,
            weight: fontAsset.weight,
            style: fontAsset.style,
          ));
        } else {
          packageFontAssets.add(FontAsset(
            Uri(pathSegments: <String>['packages', packageName, ...assetUri.pathSegments]),
            weight: fontAsset.weight,
            style: fontAsset.style,
          ));
        }
      }
      packageFonts.add(Font('packages/$packageName/${font.familyName}', packageFontAssets));
    }
    return packageFonts;
  }

  /// Given an assetBase location and a pubspec.yaml Flutter manifest, return a
  /// map of assets to asset variants.
  ///
  /// Returns null on missing assets.
  ///
  /// Given package: 'test_package' and an assets directory like this:
  ///
  /// - assets/foo
  /// - assets/var1/foo
  /// - assets/var2/foo
  /// - assets/bar
  ///
  /// This will return:
  /// ```
  /// {
  ///   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,
  ///   ],
  /// }
  /// ```
787
  Map<_Asset, List<_Asset>>? _parseAssets(
788 789 790
    PackageConfig packageConfig,
    FlutterManifest flutterManifest,
    List<Uri> wildcardDirectories,
791 792
    String assetBase,
    TargetPlatform? targetPlatform, {
793 794
    String? packageName,
    Package? attributedPackage,
795 796 797
  }) {
    final Map<_Asset, List<_Asset>> result = <_Asset, List<_Asset>>{};

798
    final _AssetDirectoryCache cache = _AssetDirectoryCache(_fileSystem);
799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825
    for (final Uri assetUri in flutterManifest.assets) {
      if (assetUri.path.endsWith('/')) {
        wildcardDirectories.add(assetUri);
        _parseAssetsFromFolder(
          packageConfig,
          flutterManifest,
          assetBase,
          cache,
          result,
          assetUri,
          packageName: packageName,
          attributedPackage: attributedPackage,
        );
      } else {
        _parseAssetFromFile(
          packageConfig,
          flutterManifest,
          assetBase,
          cache,
          result,
          assetUri,
          packageName: packageName,
          attributedPackage: attributedPackage,
        );
      }
    }

826 827 828 829 830 831 832 833 834 835 836 837
    for (final Uri shaderUri in flutterManifest.shaders) {
      _parseAssetFromFile(
        packageConfig,
        flutterManifest,
        assetBase,
        cache,
        result,
        shaderUri,
        packageName: packageName,
        attributedPackage: attributedPackage,
        assetKind: AssetKind.shader,
      );
838
    }
839

840 841 842 843 844 845 846 847 848 849 850 851 852 853
    for (final Uri modelUri in flutterManifest.models) {
      _parseAssetFromFile(
        packageConfig,
        flutterManifest,
        assetBase,
        cache,
        result,
        modelUri,
        packageName: packageName,
        attributedPackage: attributedPackage,
        assetKind: AssetKind.model,
      );
    }

854 855 856 857 858 859 860 861 862
    // Add assets referenced in the fonts section of the manifest.
    for (final Font font in flutterManifest.fonts) {
      for (final FontAsset fontAsset in font.fontAssets) {
        final _Asset baseAsset = _resolveAsset(
          packageConfig,
          assetBase,
          fontAsset.assetUri,
          packageName,
          attributedPackage,
863
          assetKind: AssetKind.font,
864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882
        );
        final File baseAssetFile = baseAsset.lookupAssetFile(_fileSystem);
        if (!baseAssetFile.existsSync()) {
          _logger.printError('Error: unable to locate asset entry in pubspec.yaml: "${fontAsset.assetUri}".');
          return null;
        }
        result[baseAsset] = <_Asset>[];
      }
    }
    return result;
  }

  void _parseAssetsFromFolder(
    PackageConfig packageConfig,
    FlutterManifest flutterManifest,
    String assetBase,
    _AssetDirectoryCache cache,
    Map<_Asset, List<_Asset>> result,
    Uri assetUri, {
883 884
    String? packageName,
    Package? attributedPackage,
885 886 887 888 889 890 891 892 893
  }) {
    final String directoryPath = _fileSystem.path.join(
        assetBase, assetUri.toFilePath(windows: _platform.isWindows));

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

894 895 896
    final Iterable<FileSystemEntity> entities = _fileSystem.directory(directoryPath).listSync();

    final Iterable<File> files = entities.whereType<File>();
897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921
    for (final File file in files) {
      final String relativePath = _fileSystem.path.relative(file.path, from: assetBase);
      final Uri uri = Uri.file(relativePath, windows: _platform.isWindows);

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

  void _parseAssetFromFile(
    PackageConfig packageConfig,
    FlutterManifest flutterManifest,
    String assetBase,
    _AssetDirectoryCache cache,
    Map<_Asset, List<_Asset>> result,
    Uri assetUri, {
    List<String> excludeDirs = const <String>[],
922 923
    String? packageName,
    Package? attributedPackage,
924
    AssetKind assetKind = AssetKind.regular,
925 926 927 928 929 930 931
  }) {
    final _Asset asset = _resolveAsset(
      packageConfig,
      assetBase,
      assetUri,
      packageName,
      attributedPackage,
932
      assetKind: assetKind,
933 934 935 936 937 938
    );
    final List<_Asset> variants = <_Asset>[];
    final File assetFile = asset.lookupAssetFile(_fileSystem);
    for (final String path in cache.variantsFor(assetFile.path)) {
      final String relativePath = _fileSystem.path.relative(path, from: asset.baseDir);
      final Uri relativeUri = _fileSystem.path.toUri(relativePath);
939
      final Uri? entryUri = asset.symbolicPrefixUri == null
940
          ? relativeUri
941 942 943 944 945 946 947 948
          : asset.symbolicPrefixUri?.resolveUri(relativeUri);
      if (entryUri != null) {
        variants.add(
          _Asset(
            baseDir: asset.baseDir,
            entryUri: entryUri,
            relativeUri: relativeUri,
            package: attributedPackage,
949
            assetKind: assetKind,
950 951 952
          ),
        );
      }
953 954 955 956 957 958 959 960 961
    }

    result[asset] = variants;
  }

  _Asset _resolveAsset(
    PackageConfig packageConfig,
    String assetsBaseDir,
    Uri assetUri,
962
    String? packageName,
963 964 965
    Package? attributedPackage, {
    AssetKind assetKind = AssetKind.regular,
  }) {
966 967 968 969 970
    final String assetPath = _fileSystem.path.fromUri(assetUri);
    if (assetUri.pathSegments.first == 'packages'
      && !_fileSystem.isFileSync(_fileSystem.path.join(assetsBaseDir, assetPath))) {
      // The asset is referenced in the pubspec.yaml as
      // 'packages/PACKAGE_NAME/PATH/TO/ASSET .
971
      final _Asset? packageAsset = _resolvePackageAsset(
972 973 974
        assetUri,
        packageConfig,
        attributedPackage,
975
        assetKind: assetKind,
976 977 978 979 980 981 982 983 984 985 986 987 988
      );
      if (packageAsset != null) {
        return packageAsset;
      }
    }

    return _Asset(
      baseDir: assetsBaseDir,
      entryUri: packageName == null
          ? assetUri // Asset from the current application.
          : Uri(pathSegments: <String>['packages', packageName, ...assetUri.pathSegments]), // Asset from, and declared in $packageName.
      relativeUri: assetUri,
      package: attributedPackage,
989
      assetKind: assetKind,
990 991 992
    );
  }

993 994 995 996 997 998
  _Asset? _resolvePackageAsset(
    Uri assetUri,
    PackageConfig packageConfig,
    Package? attributedPackage, {
    AssetKind assetKind = AssetKind.regular,
  }) {
999 1000 1001
    assert(assetUri.pathSegments.first == 'packages');
    if (assetUri.pathSegments.length > 1) {
      final String packageName = assetUri.pathSegments[1];
1002 1003
      final Package? package = packageConfig[packageName];
      final Uri? packageUri = package?.packageUriRoot;
1004 1005 1006 1007 1008 1009
      if (packageUri != null && packageUri.scheme == 'file') {
        return _Asset(
          baseDir: _fileSystem.path.fromUri(packageUri),
          entryUri: assetUri,
          relativeUri: Uri(pathSegments: assetUri.pathSegments.sublist(2)),
          package: attributedPackage,
1010
          assetKind: assetKind,
1011 1012 1013 1014 1015 1016 1017 1018 1019 1020
        );
      }
    }
    _logger.printStatus('Error detected in pubspec.yaml:', emphasis: true);
    _logger.printError('Could not resolve package for asset $assetUri.\n');
    if (attributedPackage != null) {
      _logger.printError('This asset was included from package ${attributedPackage.name}');
    }
    return null;
  }
1021 1022
}

1023
@immutable
1024
class _Asset {
1025
  const _Asset({
1026 1027 1028 1029
    required this.baseDir,
    required this.relativeUri,
    required this.entryUri,
    required this.package,
1030
    this.assetKind = AssetKind.regular,
1031
  });
1032

1033
  final String baseDir;
1034

1035
  final Package? package;
1036

1037
  /// A platform-independent URL where this asset can be found on disk on the
1038 1039
  /// host system relative to [baseDir].
  final Uri relativeUri;
1040

1041
  /// A platform-independent URL representing the entry for the asset manifest.
1042
  final Uri entryUri;
1043

1044 1045
  final AssetKind assetKind;

1046 1047
  File lookupAssetFile(FileSystem fileSystem) {
    return fileSystem.file(fileSystem.path.join(baseDir, fileSystem.path.fromUri(relativeUri)));
1048 1049
  }

1050
  /// The delta between what the entryUri is and the relativeUri (e.g.,
1051
  /// packages/flutter_gallery).
1052
  Uri? get symbolicPrefixUri {
1053
    if (entryUri == relativeUri) {
1054
      return null;
1055
    }
1056
    final int index = entryUri.path.indexOf(relativeUri.path);
1057
    return index == -1 ? null : Uri(path: entryUri.path.substring(0, index));
1058 1059 1060
  }

  @override
1061
  String toString() => 'asset: $entryUri';
1062 1063

  @override
1064
  bool operator ==(Object other) {
1065
    if (identical(other, this)) {
1066
      return true;
1067 1068
    }
    if (other.runtimeType != runtimeType) {
1069
      return false;
1070
    }
1071 1072 1073
    return other is _Asset
        && other.baseDir == baseDir
        && other.relativeUri == relativeUri
1074 1075
        && other.entryUri == entryUri
        && other.assetKind == assetKind;
1076 1077 1078
  }

  @override
1079
  int get hashCode => Object.hash(baseDir, relativeUri, entryUri.hashCode);
1080 1081
}

1082 1083
// Given an assets directory like this:
//
1084 1085 1086 1087 1088
// assets/foo.png
// assets/2x/foo.png
// assets/3.0x/foo.png
// assets/bar/foo.png
// assets/bar.png
1089
//
1090 1091 1092
// variantsFor('assets/foo.png') => ['/assets/foo.png', '/assets/2x/foo.png', 'assets/3.0x/foo.png']
// variantsFor('assets/bar.png') => ['/assets/bar.png']
// variantsFor('assets/bar/foo.png') => ['/assets/bar/foo.png']
1093
class _AssetDirectoryCache {
1094
  _AssetDirectoryCache(this._fileSystem);
1095

1096
  final FileSystem _fileSystem;
1097
  final Map<String, List<String>> _cache = <String, List<String>>{};
1098
  final Map<String, List<File>> _variantsPerFolder = <String, List<File>>{};
1099 1100

  List<String> variantsFor(String assetPath) {
1101
    final String directory = _fileSystem.path.dirname(assetPath);
1102

1103
    if (!_fileSystem.directory(directory).existsSync()) {
1104
      return const <String>[];
1105
    }
1106

1107 1108
    if (_cache.containsKey(assetPath)) {
      return _cache[assetPath]!;
1109
    }
1110 1111 1112 1113 1114 1115 1116 1117 1118
    if (!_variantsPerFolder.containsKey(directory)) {
      _variantsPerFolder[directory] = _fileSystem.directory(directory)
        .listSync()
        .whereType<Directory>()
        .where((Directory dir) => _assetVariantDirectoryRegExp.hasMatch(dir.basename))
        .expand((Directory dir) => dir.listSync())
        .whereType<File>()
        .toList();
    }
1119
    final File assetFile = _fileSystem.file(assetPath);
1120 1121 1122
    final List<File> potentialVariants = _variantsPerFolder[directory]!;
    final String basename = assetFile.basename;
    return _cache[assetPath] = <String>[
1123 1124
      // It's possible that the user specifies only explicit variants (e.g. .../1x/asset.png),
      // so there does not necessarily need to be a file at the given path.
1125
      if (assetFile.existsSync())
1126
        assetPath,
1127 1128
      ...potentialVariants
        .where((File file) => file.basename == basename)
1129 1130
        .map((File file) => file.path),
    ];
1131 1132
  }
}