asset.dart 39.2 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
  // We assume the main asset is designed for a device pixel ratio of 1.0.
  static const String _kAssetManifestJsonFilename = 'AssetManifest.json';
170
  static const String _kAssetManifestBinFilename = 'AssetManifest.bin';
171

172
  static const String _kNoticeFile = 'NOTICES';
173 174 175 176 177 178 179
  // 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';
180

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

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

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

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

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

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

220
    if (flutterProject == null) {
221 222 223 224 225 226 227
      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;
      }
228
    }
229

230
    final FlutterManifest flutterManifest = flutterProject.manifest;
231 232 233 234
    // 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();
235
    if (flutterManifest.isEmpty) {
236 237 238 239 240 241 242 243 244
      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;
245 246 247
      return 0;
    }

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

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

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

273
    // Parse assets for deferred components.
274 275 276 277 278 279 280 281 282 283
    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.
284
      deferredComponentsAssetVariants.values.forEach(assetVariants.addAll);
285 286 287 288
      deferredComponentsAssetVariants.clear();
      deferredComponentsEntries.clear();
    }

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

296 297
    // Add fonts, assets, and licenses from packages.
    final Map<String, List<File>> additionalLicenseFiles = <String, List<File>>{};
298 299
    for (final Package package in packageConfig.packages) {
      final Uri packageUri = package.packageUriRoot;
300
      if (packageUri.scheme == 'file') {
301
        final String packageManifestPath = _fileSystem.path.fromUri(packageUri.resolve('../pubspec.yaml'));
302
        inputFiles.add(_fileSystem.file(packageManifestPath));
303
        final FlutterManifest? packageFlutterManifest = FlutterManifest.createFromPath(
304
          packageManifestPath,
305 306
          logger: _logger,
          fileSystem: _fileSystem,
307
        );
308
        if (packageFlutterManifest == null) {
309
          continue;
310
        }
311 312 313 314 315 316 317 318
        // 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;

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

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

336
        if (packageAssets == null) {
337
          return 1;
338
        }
339
        assetVariants.addAll(packageAssets);
340
        if (!includesMaterialFonts && packageFlutterManifest.usesMaterialDesign) {
341
          _logger.printError(
342 343 344 345 346 347
            '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.'
          );
        }
348 349
        fonts.addAll(_parseFonts(
          packageFlutterManifest,
350 351
          packageConfig,
          packageName: package.name,
352
          primary: false,
353
        ));
354 355 356
      }
    }

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

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

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

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

452 453 454 455
    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 .
456
      _logger.printTrace(
457 458 459 460 461
        '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(
462
        _fileSystem.file('DOES_NOT_EXIST_RERUN_FOR_WILDCARD$suffix').absolute);
463 464
    }

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

  @override
  List<File> additionalDependencies = <File>[];
474 475 476 477 478 479
  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)) {
480
      return;
481
    }
482 483 484 485 486 487 488 489

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

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

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

    return true;
499
  }
500

501 502
  void _setLicenseIfChanged(
    String combinedLicenses,
503
    TargetPlatform? targetPlatform,
504 505 506 507 508
  ) {
    // 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) {
509
      _setIfChanged(_kNoticeFile, DevFSStringContent(combinedLicenses), AssetKind.regular);
510 511 512 513 514 515 516 517
      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) ||
518
        (entries[_kNoticeZippedFile] as DevFSStringCompressingBytesContent?)
519
            ?.equals(combinedLicenses) != true) {
520 521 522 523 524 525 526
      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',
      );
527
      entryKinds[_kNoticeZippedFile] = AssetKind.regular;
528 529 530
    }
  }

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

    return result;
  }

560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579
  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,
580
        assetKind: AssetKind.shader,
581 582 583 584 585 586
      ));
    }

    return result;
  }

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

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

650
  Map<String, List<String>> _createAssetManifest(
651 652 653
    Map<_Asset, List<_Asset>> assetVariants,
    Map<String, Map<_Asset, List<_Asset>>> deferredComponentsAssetVariants
  ) {
654 655
    final Map<String, List<String>> manifest = <String, List<String>>{};
    final Map<_Asset, List<String>> entries = <_Asset, List<String>>{};
656
    assetVariants.forEach((_Asset main, List<_Asset> variants) {
657
      entries[main] = <String>[
658
        for (final _Asset variant in variants)
659 660
          variant.entryUri.path,
      ];
661
    });
662 663 664 665 666 667 668
    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,
        ];
      });
669
    }
670
    final List<_Asset> sortedKeys = entries.keys.toList()
671 672
        ..sort((_Asset left, _Asset right) => left.entryUri.path.compareTo(right.entryUri.path));
    for (final _Asset main in sortedKeys) {
673
      final String decodedEntryPath = Uri.decodeFull(main.entryUri.path);
674
      final List<String> rawEntryVariantsPaths = entries[main]!;
675 676 677
      final List<String> decodedEntryVariantPaths = rawEntryVariantsPaths
        .map((String value) => Uri.decodeFull(value))
        .toList();
678
      manifest[decodedEntryPath] = decodedEntryVariantPaths;
679
    }
680 681 682 683 684 685 686 687 688 689
    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
  ) {
690
    double? parseScale(String key) {
691 692 693 694 695 696 697 698 699 700
      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)!);
      }
701 702

      return null;
703 704 705 706 707 708 709 710 711
    }

    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) {
        final Map<String, dynamic> resultVariant = <String, dynamic>{};
712
        final double? variantDevicePixelRatio = parseScale(variant);
713
        resultVariant['asset'] = variant;
714 715 716
        if (variantDevicePixelRatio != null) {
          resultVariant['dpr'] = variantDevicePixelRatio;
        }
717 718 719 720 721 722 723
        resultVariants.add(resultVariant);
      }
      result[manifestEntry.key] = resultVariants;
    }

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

  /// 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(
740
              packageConfig[packageName]?.packageUriRoot.resolve('../${assetUri.path}')))) {
741 742 743 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
          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,
  ///   ],
  /// }
  /// ```
784
  Map<_Asset, List<_Asset>>? _parseAssets(
785 786 787
    PackageConfig packageConfig,
    FlutterManifest flutterManifest,
    List<Uri> wildcardDirectories,
788 789
    String assetBase,
    TargetPlatform? targetPlatform, {
790 791
    String? packageName,
    Package? attributedPackage,
792 793 794
  }) {
    final Map<_Asset, List<_Asset>> result = <_Asset, List<_Asset>>{};

795
    final _AssetDirectoryCache cache = _AssetDirectoryCache(_fileSystem);
796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822
    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,
        );
      }
    }

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

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

851 852 853 854 855 856 857 858 859
    // 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,
860
          assetKind: AssetKind.font,
861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879
        );
        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, {
880 881
    String? packageName,
    Package? attributedPackage,
882 883 884 885 886 887 888 889 890
  }) {
    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;
    }

891 892 893
    final Iterable<FileSystemEntity> entities = _fileSystem.directory(directoryPath).listSync();

    final Iterable<File> files = entities.whereType<File>();
894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918
    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>[],
919 920
    String? packageName,
    Package? attributedPackage,
921
    AssetKind assetKind = AssetKind.regular,
922 923 924 925 926 927 928
  }) {
    final _Asset asset = _resolveAsset(
      packageConfig,
      assetBase,
      assetUri,
      packageName,
      attributedPackage,
929
      assetKind: assetKind,
930 931 932 933 934 935
    );
    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);
936
      final Uri? entryUri = asset.symbolicPrefixUri == null
937
          ? relativeUri
938 939 940 941 942 943 944 945
          : asset.symbolicPrefixUri?.resolveUri(relativeUri);
      if (entryUri != null) {
        variants.add(
          _Asset(
            baseDir: asset.baseDir,
            entryUri: entryUri,
            relativeUri: relativeUri,
            package: attributedPackage,
946
            assetKind: assetKind,
947 948 949
          ),
        );
      }
950 951 952 953 954 955 956 957 958
    }

    result[asset] = variants;
  }

  _Asset _resolveAsset(
    PackageConfig packageConfig,
    String assetsBaseDir,
    Uri assetUri,
959
    String? packageName,
960 961 962
    Package? attributedPackage, {
    AssetKind assetKind = AssetKind.regular,
  }) {
963 964 965 966 967
    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 .
968
      final _Asset? packageAsset = _resolvePackageAsset(
969 970 971
        assetUri,
        packageConfig,
        attributedPackage,
972
        assetKind: assetKind,
973 974 975 976 977 978 979 980 981 982 983 984 985
      );
      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,
986
      assetKind: assetKind,
987 988 989
    );
  }

990 991 992 993 994 995
  _Asset? _resolvePackageAsset(
    Uri assetUri,
    PackageConfig packageConfig,
    Package? attributedPackage, {
    AssetKind assetKind = AssetKind.regular,
  }) {
996 997 998
    assert(assetUri.pathSegments.first == 'packages');
    if (assetUri.pathSegments.length > 1) {
      final String packageName = assetUri.pathSegments[1];
999 1000
      final Package? package = packageConfig[packageName];
      final Uri? packageUri = package?.packageUriRoot;
1001 1002 1003 1004 1005 1006
      if (packageUri != null && packageUri.scheme == 'file') {
        return _Asset(
          baseDir: _fileSystem.path.fromUri(packageUri),
          entryUri: assetUri,
          relativeUri: Uri(pathSegments: assetUri.pathSegments.sublist(2)),
          package: attributedPackage,
1007
          assetKind: assetKind,
1008 1009 1010 1011 1012 1013 1014 1015 1016 1017
        );
      }
    }
    _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;
  }
1018 1019
}

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

1030
  final String baseDir;
1031

1032
  final Package? package;
1033

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

1038
  /// A platform-independent URL representing the entry for the asset manifest.
1039
  final Uri entryUri;
1040

1041 1042
  final AssetKind assetKind;

1043 1044
  File lookupAssetFile(FileSystem fileSystem) {
    return fileSystem.file(fileSystem.path.join(baseDir, fileSystem.path.fromUri(relativeUri)));
1045 1046
  }

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

  @override
1058
  String toString() => 'asset: $entryUri';
1059 1060

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

  @override
1076
  int get hashCode => Object.hash(baseDir, relativeUri, entryUri.hashCode);
1077 1078
}

1079 1080
// Given an assets directory like this:
//
1081 1082 1083 1084 1085
// assets/foo.png
// assets/2x/foo.png
// assets/3.0x/foo.png
// assets/bar/foo.png
// assets/bar.png
1086
//
1087 1088 1089
// 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']
1090
class _AssetDirectoryCache {
1091
  _AssetDirectoryCache(this._fileSystem);
1092

1093
  final FileSystem _fileSystem;
1094
  final Map<String, List<String>> _cache = <String, List<String>>{};
1095
  final Map<String, List<File>> _variantsPerFolder = <String, List<File>>{};
1096 1097

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

1100
    if (!_fileSystem.directory(directory).existsSync()) {
1101
      return const <String>[];
1102
    }
1103

1104 1105
    if (_cache.containsKey(assetPath)) {
      return _cache[assetPath]!;
1106
    }
1107 1108 1109 1110 1111 1112 1113 1114 1115
    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();
    }
1116
    final File assetFile = _fileSystem.file(assetPath);
1117 1118 1119
    final List<File> potentialVariants = _variantsPerFolder[directory]!;
    final String basename = assetFile.basename;
    return _cache[assetPath] = <String>[
1120 1121
      // 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.
1122
      if (assetFile.existsSync())
1123
        assetPath,
1124 1125
      ...potentialVariants
        .where((File file) => file.basename == basename)
1126 1127
        .map((File file) => file.path),
    ];
1128 1129
  }
}