asset.dart 37 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
import 'package:meta/meta.dart';
6
import 'package:package_config/package_config.dart';
7

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

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

24 25
const String kFontManifestJson = 'FontManifest.json';

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

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

49 50 51 52
const List<String> kMaterialShaders = <String>[
  'shaders/ink_sparkle.frag',
];

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

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

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

69 70 71 72 73 74
enum AssetKind {
  regular,
  font,
  shader,
}

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

78 79
  Map<String, AssetKind> get entryKinds;

80 81 82 83
  /// The files that were specified under the deferred components assets sections
  /// in pubspec.
  Map<String, Map<String, DevFSContent>> get deferredComponentsEntries;

84 85 86 87
  /// Additional files that this bundle depends on that are not included in the
  /// output result.
  List<File> get additionalDependencies;

88 89 90
  /// Input files used to build this asset bundle.
  List<File> get inputFiles;

91 92
  bool wasBuiltOnce();

93
  bool needsBuild({ String manifestPath = defaultManifestPath });
94 95 96

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

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

  final Logger _logger;
  final FileSystem _fileSystem;
117
  final Platform _platform;
118
  final bool _splitDeferredAssets;
119 120

  @override
121
  AssetBundle createBundle() => ManifestAssetBundle(logger: _logger, fileSystem: _fileSystem, platform: _platform, splitDeferredAssets: _splitDeferredAssets);
122 123
}

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

  final Logger _logger;
  final FileSystem _fileSystem;
  final LicenseCollector _licenseCollector;
142
  final Platform _platform;
143
  final bool _splitDeferredAssets;
144

145
  @override
146
  final Map<String, DevFSContent> entries = <String, DevFSContent>{};
147

148 149 150
  @override
  final Map<String, AssetKind> entryKinds = <String, AssetKind>{};

151 152 153
  @override
  final Map<String, Map<String, DevFSContent>> deferredComponentsEntries = <String, Map<String, DevFSContent>>{};

154 155 156
  @override
  final List<File> inputFiles = <File>[];

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

162
  DateTime? _lastBuildTimestamp;
163

164
  static const String _kAssetManifestJson = 'AssetManifest.json';
165
  static const String _kNoticeFile = 'NOTICES';
166 167 168 169 170 171 172
  // 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';
173

174 175 176
  @override
  bool wasBuiltOnce() => _lastBuildTimestamp != null;

177
  @override
178
  bool needsBuild({ String manifestPath = defaultManifestPath }) {
179
    final DateTime? lastBuildTimestamp = _lastBuildTimestamp;
180
    if (lastBuildTimestamp == null) {
181
      return true;
182
    }
183

184
    final FileStat stat = _fileSystem.file(manifestPath).statSync();
185
    if (stat.type == FileSystemEntityType.notFound) {
186
      return true;
187
    }
188

189
    for (final Directory directory in _wildcardDirectories.values) {
190 191
      if (!directory.existsSync()) {
        return true; // directory was deleted.
192
      }
193
      for (final File file in directory.listSync().whereType<File>()) {
194 195 196 197
        final DateTime dateTime = file.statSync().modified;
        if (dateTime == null) {
          continue;
        }
198
        if (dateTime.isAfter(lastBuildTimestamp)) {
199 200
          return true;
        }
201 202 203
      }
    }

204
    return stat.modified.isAfter(lastBuildTimestamp);
205 206
  }

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

216
    if (flutterProject == null) {
217 218 219 220 221 222 223
      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;
      }
224
    }
225

226
    final FlutterManifest flutterManifest = flutterProject.manifest;
227 228 229 230
    // 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();
231
    if (flutterManifest.isEmpty) {
232 233
      entries[_kAssetManifestJson] = DevFSStringContent('{}');
      entryKinds[_kAssetManifestJson] = AssetKind.regular;
234 235 236
      return 0;
    }

237
    final String assetBasePath = _fileSystem.path.dirname(_fileSystem.path.absolute(manifestPath));
238 239
    final File packageConfigFile = _fileSystem.file(packagesPath);
    inputFiles.add(packageConfigFile);
240
    final PackageConfig packageConfig = await loadPackageConfigWithLogging(
241
      packageConfigFile,
242
      logger: _logger,
243
    );
244
    final List<Uri> wildcardDirectories = <Uri>[];
245

246
    // The _assetVariants map contains an entry for each asset listed
247
    // in the pubspec.yaml file's assets and font sections. The
248 249
    // value of each image asset is a list of resolution-specific "variants",
    // see _AssetDirectoryCache.
250
    final Map<_Asset, List<_Asset>>? assetVariants = _parseAssets(
251
      packageConfig,
252
      flutterManifest,
253
      wildcardDirectories,
254
      assetBasePath,
255
      targetPlatform,
256 257
    );

258
    if (assetVariants == null) {
259
      return 1;
260
    }
261

262
    // Parse assets for deferred components.
263 264 265 266 267 268 269 270 271 272
    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.
273
      deferredComponentsAssetVariants.values.forEach(assetVariants.addAll);
274 275 276 277
      deferredComponentsAssetVariants.clear();
      deferredComponentsEntries.clear();
    }

278
    final bool includesMaterialFonts = flutterManifest.usesMaterialDesign;
279
    final List<Map<String, Object?>> fonts = _parseFonts(
280
      flutterManifest,
281
      packageConfig,
282
      primary: true,
283
    );
284

285 286
    // Add fonts, assets, and licenses from packages.
    final Map<String, List<File>> additionalLicenseFiles = <String, List<File>>{};
287 288 289
    for (final Package package in packageConfig.packages) {
      final Uri packageUri = package.packageUriRoot;
      if (packageUri != null && packageUri.scheme == 'file') {
290
        final String packageManifestPath = _fileSystem.path.fromUri(packageUri.resolve('../pubspec.yaml'));
291
        inputFiles.add(_fileSystem.file(packageManifestPath));
292
        final FlutterManifest? packageFlutterManifest = FlutterManifest.createFromPath(
293
          packageManifestPath,
294 295
          logger: _logger,
          fileSystem: _fileSystem,
296
        );
297
        if (packageFlutterManifest == null) {
298
          continue;
299
        }
300 301 302 303 304 305 306 307
        // 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;

308
        // Skip the app itself
309
        if (packageFlutterManifest.appName == flutterManifest.appName) {
310
          continue;
311
        }
312
        final String packageBasePath = _fileSystem.path.dirname(packageManifestPath);
313

314
        final Map<_Asset, List<_Asset>>? packageAssets = _parseAssets(
315
          packageConfig,
316
          packageFlutterManifest,
317 318
          // Do not track wildcard directories for dependencies.
          <Uri>[],
319
          packageBasePath,
320
          targetPlatform,
321
          packageName: package.name,
322
          attributedPackage: package,
323 324
        );

325
        if (packageAssets == null) {
326
          return 1;
327
        }
328
        assetVariants.addAll(packageAssets);
329
        if (!includesMaterialFonts && packageFlutterManifest.usesMaterialDesign) {
330
          _logger.printError(
331 332 333 334 335 336
            '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.'
          );
        }
337 338
        fonts.addAll(_parseFonts(
          packageFlutterManifest,
339 340
          packageConfig,
          packageName: package.name,
341
          primary: false,
342
        ));
343 344 345
      }
    }

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

425
    // Update wildcard directories we can detect changes in them.
426
    for (final Uri uri in wildcardDirectories) {
427
      _wildcardDirectories[uri] ??= _fileSystem.directory(uri);
428 429
    }

430
    final DevFSStringContent assetManifest  = _createAssetManifest(assetVariants, deferredComponentsAssetVariants);
431
    final DevFSStringContent fontManifest = DevFSStringContent(json.encode(fonts));
432 433 434 435 436 437
    final LicenseResult licenseResult = _licenseCollector.obtainLicenses(packageConfig, additionalLicenseFiles);
    if (licenseResult.errorMessages.isNotEmpty) {
      licenseResult.errorMessages.forEach(_logger.printError);
      return 1;
    }

438
    additionalDependencies = licenseResult.dependencies;
439
    inputFiles.addAll(additionalDependencies);
440

441 442 443 444
    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 .
445
      _logger.printTrace(
446 447 448 449 450
        '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(
451
        _fileSystem.file('DOES_NOT_EXIST_RERUN_FOR_WILDCARD$suffix').absolute);
452 453
    }

454
    _setIfChanged(_kAssetManifestJson, assetManifest, AssetKind.regular);
455
    _setIfChanged(kFontManifestJson, fontManifest, AssetKind.regular);
456
    _setLicenseIfChanged(licenseResult.combinedLicenses, targetPlatform);
457 458
    return 0;
  }
459 460 461

  @override
  List<File> additionalDependencies = <File>[];
462

463 464 465 466
  void _setIfChanged(String key, DevFSStringContent content, AssetKind assetKind) {
    if (!entries.containsKey(key)) {
      entries[key] = content;
      entryKinds[key] = assetKind;
467
      return;
468
    }
469 470 471 472 473
    final DevFSStringContent? oldContent = entries[key] as DevFSStringContent?;
    if (oldContent?.string != content.string) {
      entries[key] = content;
      entryKinds[key] = assetKind;
    }
474
  }
475

476 477
  void _setLicenseIfChanged(
    String combinedLicenses,
478
    TargetPlatform? targetPlatform,
479 480 481 482 483
  ) {
    // 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) {
484
      _setIfChanged(_kNoticeFile, DevFSStringContent(combinedLicenses), AssetKind.regular);
485 486 487 488 489 490 491 492
      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) ||
493
        (entries[_kNoticeZippedFile] as DevFSStringCompressingBytesContent?)
494
            ?.equals(combinedLicenses) != true) {
495 496 497 498 499 500 501
      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',
      );
502
      entryKinds[_kNoticeZippedFile] = AssetKind.regular;
503 504 505
    }
  }

506
  List<_Asset> _getMaterialFonts() {
507 508
    final List<_Asset> result = <_Asset>[];
    for (final Map<String, Object> family in kMaterialFonts) {
509
      final Object? fonts = family['fonts'];
510 511 512 513
      if (fonts == null) {
        continue;
      }
      for (final Map<String, Object> font in fonts as List<Map<String, String>>) {
514
        final String? asset = font['asset'] as String?;
515 516 517 518
        if (asset == null) {
          continue;
        }
        final Uri entryUri = _fileSystem.path.toUri(asset);
519
        result.add(_Asset(
520 521 522 523
          baseDir: _fileSystem.path.join(
            Cache.flutterRoot!,
            'bin', 'cache', 'artifacts', 'material_fonts',
          ),
524 525 526
          relativeUri: Uri(path: entryUri.pathSegments.last),
          entryUri: entryUri,
          package: null,
527
          assetKind: AssetKind.font,
528 529 530 531 532 533 534
        ));
      }
    }

    return result;
  }

535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554
  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,
555
        assetKind: AssetKind.shader,
556 557 558 559 560 561
      ));
    }

    return result;
  }

562
  List<Map<String, Object?>> _parseFonts(
563 564
    FlutterManifest manifest,
    PackageConfig packageConfig, {
565 566
    String? packageName,
    required bool primary,
567
  }) {
568
    return <Map<String, Object?>>[
569
      if (primary && manifest.usesMaterialDesign)
570 571 572 573 574 575 576 577 578 579 580
        ...kMaterialFonts,
      if (packageName == null)
        ...manifest.fontsDescriptor
      else
        for (Font font in _parsePackageFonts(
          manifest,
          packageName,
          packageConfig,
        )) font.descriptor,
    ];
  }
581

582 583 584 585 586 587 588 589
  Map<String, Map<_Asset, List<_Asset>>> _parseDeferredComponentsAssets(
    FlutterManifest flutterManifest,
    PackageConfig packageConfig,
    String assetBasePath,
    List<Uri> wildcardDirectories,
    Directory projectDirectory, {
    List<String> excludeDirs = const <String>[],
  }) {
590
    final List<DeferredComponent>? components = flutterManifest.deferredComponents;
591 592 593 594 595 596
    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>>{};
597
      final _AssetDirectoryCache cache = _AssetDirectoryCache(_fileSystem);
598 599 600 601 602 603 604 605
      for (final Uri assetUri in component.assets) {
        if (assetUri.path.endsWith('/')) {
          wildcardDirectories.add(assetUri);
          _parseAssetsFromFolder(
            packageConfig,
            flutterManifest,
            assetBasePath,
            cache,
606
            deferredComponentsAssetVariants[component.name]!,
607 608 609 610 611 612 613 614
            assetUri,
          );
        } else {
          _parseAssetFromFile(
            packageConfig,
            flutterManifest,
            assetBasePath,
            cache,
615
            deferredComponentsAssetVariants[component.name]!,
616 617 618 619 620 621 622 623 624
            assetUri,
            excludeDirs: excludeDirs,
          );
        }
      }
    }
    return deferredComponentsAssetVariants;
  }

625
  DevFSStringContent _createAssetManifest(
626 627 628
    Map<_Asset, List<_Asset>> assetVariants,
    Map<String, Map<_Asset, List<_Asset>>> deferredComponentsAssetVariants
  ) {
629 630
    final Map<String, List<String>> jsonObject = <String, List<String>>{};
    final Map<_Asset, List<String>> jsonEntries = <_Asset, List<String>>{};
631
    assetVariants.forEach((_Asset main, List<_Asset> variants) {
632
      jsonEntries[main] = <String>[
633
        for (final _Asset variant in variants)
634 635
          variant.entryUri.path,
      ];
636
    });
637 638
    if (deferredComponentsAssetVariants != null) {
      for (final Map<_Asset, List<_Asset>> componentAssets in deferredComponentsAssetVariants.values) {
639
        componentAssets.forEach((_Asset main, List<_Asset> variants) {
640
          jsonEntries[main] = <String>[
641
            for (final _Asset variant in variants)
642 643
              variant.entryUri.path,
          ];
644
        });
645 646
      }
    }
647
    final List<_Asset> sortedKeys = jsonEntries.keys.toList()
648 649
        ..sort((_Asset left, _Asset right) => left.entryUri.path.compareTo(right.entryUri.path));
    for (final _Asset main in sortedKeys) {
650
      final String decodedEntryPath = Uri.decodeFull(main.entryUri.path);
651
      final List<String> rawEntryVariantsPaths = jsonEntries[main]!;
652 653 654
      final List<String> decodedEntryVariantPaths = rawEntryVariantsPaths
        .map((String value) => Uri.decodeFull(value))
        .toList();
655
      jsonObject[decodedEntryPath] = decodedEntryVariantPaths;
656
    }
657
    return DevFSStringContent(json.encode(jsonObject));
658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673
  }

  /// 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(
674
              packageConfig[packageName]?.packageUriRoot.resolve('../${assetUri.path}')))) {
675 676 677 678 679 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
          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,
  ///   ],
  /// }
  /// ```
718
  Map<_Asset, List<_Asset>>? _parseAssets(
719 720 721
    PackageConfig packageConfig,
    FlutterManifest flutterManifest,
    List<Uri> wildcardDirectories,
722 723
    String assetBase,
    TargetPlatform? targetPlatform, {
724 725
    String? packageName,
    Package? attributedPackage,
726 727 728
  }) {
    final Map<_Asset, List<_Asset>> result = <_Asset, List<_Asset>>{};

729
    final _AssetDirectoryCache cache = _AssetDirectoryCache(_fileSystem);
730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756
    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,
        );
      }
    }

757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772
    // TODO(jonahwilliams): re-enable this feature once
    // flutter web is using engine compiled canvaskit.
    if (targetPlatform != TargetPlatform.web_javascript) {
      for (final Uri shaderUri in flutterManifest.shaders) {
        _parseAssetFromFile(
          packageConfig,
          flutterManifest,
          assetBase,
          cache,
          result,
          shaderUri,
          packageName: packageName,
          attributedPackage: attributedPackage,
          assetKind: AssetKind.shader,
        );
      }
773
    }
774

775 776 777 778 779 780 781 782 783
    // 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,
784
          assetKind: AssetKind.font,
785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803
        );
        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, {
804 805
    String? packageName,
    Package? attributedPackage,
806 807 808 809 810 811 812 813 814
  }) {
    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;
    }

815 816 817
    final Iterable<FileSystemEntity> entities = _fileSystem.directory(directoryPath).listSync();

    final Iterable<File> files = entities.whereType<File>();
818 819 820 821 822 823 824 825 826 827 828 829 830 831 832
    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,
      );
    }
833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848

    final Iterable<Directory> nonVariantSubDirectories = entities
      .whereType<Directory>()
      .where((Directory directory) => !_assetVariantDirectoryRegExp.hasMatch(directory.basename));
    for (final Directory dir in nonVariantSubDirectories) {
      final String relativePath = _fileSystem.path.relative(dir.path, from: assetBase);
      final Uri relativePathsUri = Uri.directory(relativePath, windows: _platform.isWindows);

      _parseAssetsFromFolder(packageConfig,
        flutterManifest,
        assetBase,
        cache,
        result,
        relativePathsUri
      );
    }
849 850 851 852 853 854 855 856 857 858
  }

  void _parseAssetFromFile(
    PackageConfig packageConfig,
    FlutterManifest flutterManifest,
    String assetBase,
    _AssetDirectoryCache cache,
    Map<_Asset, List<_Asset>> result,
    Uri assetUri, {
    List<String> excludeDirs = const <String>[],
859 860
    String? packageName,
    Package? attributedPackage,
861
    AssetKind assetKind = AssetKind.regular,
862 863 864 865 866 867 868
  }) {
    final _Asset asset = _resolveAsset(
      packageConfig,
      assetBase,
      assetUri,
      packageName,
      attributedPackage,
869
      assetKind: assetKind,
870 871 872 873 874 875
    );
    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);
876
      final Uri? entryUri = asset.symbolicPrefixUri == null
877
          ? relativeUri
878 879 880 881 882 883 884 885
          : asset.symbolicPrefixUri?.resolveUri(relativeUri);
      if (entryUri != null) {
        variants.add(
          _Asset(
            baseDir: asset.baseDir,
            entryUri: entryUri,
            relativeUri: relativeUri,
            package: attributedPackage,
886
            assetKind: assetKind,
887 888 889
          ),
        );
      }
890 891 892 893 894 895 896 897 898
    }

    result[asset] = variants;
  }

  _Asset _resolveAsset(
    PackageConfig packageConfig,
    String assetsBaseDir,
    Uri assetUri,
899
    String? packageName,
900 901 902
    Package? attributedPackage, {
    AssetKind assetKind = AssetKind.regular,
  }) {
903 904 905 906 907
    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 .
908
      final _Asset? packageAsset = _resolvePackageAsset(
909 910 911
        assetUri,
        packageConfig,
        attributedPackage,
912
        assetKind: assetKind,
913 914 915 916 917 918 919 920 921 922 923 924 925
      );
      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,
926
      assetKind: assetKind,
927 928 929
    );
  }

930 931 932 933 934 935
  _Asset? _resolvePackageAsset(
    Uri assetUri,
    PackageConfig packageConfig,
    Package? attributedPackage, {
    AssetKind assetKind = AssetKind.regular,
  }) {
936 937 938
    assert(assetUri.pathSegments.first == 'packages');
    if (assetUri.pathSegments.length > 1) {
      final String packageName = assetUri.pathSegments[1];
939 940
      final Package? package = packageConfig[packageName];
      final Uri? packageUri = package?.packageUriRoot;
941 942 943 944 945 946
      if (packageUri != null && packageUri.scheme == 'file') {
        return _Asset(
          baseDir: _fileSystem.path.fromUri(packageUri),
          entryUri: assetUri,
          relativeUri: Uri(pathSegments: assetUri.pathSegments.sublist(2)),
          package: attributedPackage,
947
          assetKind: assetKind,
948 949 950 951 952 953 954 955 956 957
        );
      }
    }
    _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;
  }
958 959
}

960
@immutable
961
class _Asset {
962
  const _Asset({
963 964 965 966
    required this.baseDir,
    required this.relativeUri,
    required this.entryUri,
    required this.package,
967
    this.assetKind = AssetKind.regular,
968
  });
969

970
  final String baseDir;
971

972
  final Package? package;
973

974
  /// A platform-independent URL where this asset can be found on disk on the
975 976
  /// host system relative to [baseDir].
  final Uri relativeUri;
977

978
  /// A platform-independent URL representing the entry for the asset manifest.
979
  final Uri entryUri;
980

981 982
  final AssetKind assetKind;

983 984
  File lookupAssetFile(FileSystem fileSystem) {
    return fileSystem.file(fileSystem.path.join(baseDir, fileSystem.path.fromUri(relativeUri)));
985 986
  }

987
  /// The delta between what the entryUri is and the relativeUri (e.g.,
988
  /// packages/flutter_gallery).
989
  Uri? get symbolicPrefixUri {
990
    if (entryUri == relativeUri) {
991
      return null;
992
    }
993
    final int index = entryUri.path.indexOf(relativeUri.path);
994
    return index == -1 ? null : Uri(path: entryUri.path.substring(0, index));
995 996 997
  }

  @override
998
  String toString() => 'asset: $entryUri';
999 1000

  @override
1001
  bool operator ==(Object other) {
1002
    if (identical(other, this)) {
1003
      return true;
1004 1005
    }
    if (other.runtimeType != runtimeType) {
1006
      return false;
1007
    }
1008 1009 1010
    return other is _Asset
        && other.baseDir == baseDir
        && other.relativeUri == relativeUri
1011 1012
        && other.entryUri == entryUri
        && other.assetKind == assetKind;
1013 1014 1015
  }

  @override
1016
  int get hashCode => Object.hash(baseDir, relativeUri, entryUri.hashCode);
1017 1018
}

1019 1020
// Given an assets directory like this:
//
1021 1022 1023 1024 1025
// assets/foo.png
// assets/2x/foo.png
// assets/3.0x/foo.png
// assets/bar/foo.png
// assets/bar.png
1026
//
1027 1028 1029
// 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']
1030
class _AssetDirectoryCache {
1031
  _AssetDirectoryCache(this._fileSystem);
1032

1033
  final FileSystem _fileSystem;
1034
  final Map<String, List<String>> _cache = <String, List<String>>{};
1035
  final Map<String, List<File>> _variantsPerFolder = <String, List<File>>{};
1036 1037

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

1040
    if (!_fileSystem.directory(directory).existsSync()) {
1041
      return const <String>[];
1042
    }
1043

1044 1045
    if (_cache.containsKey(assetPath)) {
      return _cache[assetPath]!;
1046
    }
1047 1048 1049 1050 1051 1052 1053 1054 1055
    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();
    }
1056
    final File assetFile = _fileSystem.file(assetPath);
1057 1058 1059
    final List<File> potentialVariants = _variantsPerFolder[directory]!;
    final String basename = assetFile.basename;
    return _cache[assetPath] = <String>[
1060 1061
      // 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.
1062
      if (assetFile.existsSync())
1063
        assetPath,
1064 1065
      ...potentialVariants
        .where((File file) => file.basename == basename)
1066 1067
        .map((File file) => file.path),
    ];
1068 1069
  }
}