1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:typed_data';
6
7import 'package:meta/meta.dart';
8import 'package:package_config/package_config.dart';
9import 'package:standard_message_codec/standard_message_codec.dart';
10
11import 'base/common.dart';
12import 'base/context.dart';
13import 'base/deferred_component.dart';
14import 'base/file_system.dart';
15import 'base/logger.dart';
16import 'base/platform.dart';
17import 'base/utils.dart';
18import 'build_info.dart';
19import 'cache.dart';
20import 'convert.dart';
21import 'dart/package_map.dart';
22import 'devfs.dart';
23import 'flutter_manifest.dart';
24import 'license_collector.dart';
25import 'package_graph.dart';
26import 'project.dart';
27
28const defaultManifestPath = 'pubspec.yaml';
29
30const kFontManifestJson = 'FontManifest.json';
31
32// Should match '2x', '/1x', '1.5x', etc.
33final _assetVariantDirectoryRegExp = RegExp(r'/?(\d+(\.\d*)?)x$');
34
35/// The effect of adding `uses-material-design: true` to the pubspec is to insert
36/// the following snippet into the asset manifest:
37///
38/// ```yaml
39/// material:
40/// - family: MaterialIcons
41/// fonts:
42/// - asset: fonts/MaterialIcons-Regular.otf
43/// ```
44const kMaterialFonts = <Map<String, Object>>[
45 <String, Object>{
46 'family': 'MaterialIcons',
47 'fonts': <Map<String, String>>[
48 <String, String>{'asset': 'fonts/MaterialIcons-Regular.otf'},
49 ],
50 },
51];
52
53const kMaterialShaders = <String>['shaders/ink_sparkle.frag'];
54
55/// Injected factory class for spawning [AssetBundle] instances.
56abstract class AssetBundleFactory {
57 /// The singleton instance, pulled from the [AppContext].
58 static AssetBundleFactory get instance => context.get<AssetBundleFactory>()!;
59
60 static AssetBundleFactory defaultInstance({
61 required Logger logger,
62 required FileSystem fileSystem,
63 required Platform platform,
64 bool splitDeferredAssets = false,
65 }) => _ManifestAssetBundleFactory(
66 logger: logger,
67 fileSystem: fileSystem,
68 platform: platform,
69 splitDeferredAssets: splitDeferredAssets,
70 );
71
72 /// Creates a new [AssetBundle].
73 AssetBundle createBundle();
74}
75
76enum AssetKind { regular, font, shader }
77
78/// Contains all information about an asset needed by tool the to prepare and
79/// copy an asset file to the build output.
80final class AssetBundleEntry {
81 AssetBundleEntry(this.content, {required this.kind, required this.transformers});
82
83 final DevFSContent content;
84 final AssetKind kind;
85 final List<AssetTransformerEntry> transformers;
86
87 Future<List<int>> contentsAsBytes() => content.contentsAsBytes();
88
89 bool hasEquivalentConfigurationWith(AssetBundleEntry other) {
90 return listEquals(transformers, other.transformers);
91 }
92}
93
94abstract class AssetBundle {
95 /// The files that were specified under the `assets` section in the pubspec,
96 /// indexed by asset key.
97 Map<String, AssetBundleEntry> get entries;
98
99 /// The files that were specified under the deferred components assets sections
100 /// in a pubspec, indexed by component name and asset key.
101 Map<String, Map<String, AssetBundleEntry>> get deferredComponentsEntries;
102
103 /// Additional files that this bundle depends on that are not included in the
104 /// output result.
105 List<File> get additionalDependencies;
106
107 /// Input files used to build this asset bundle.
108 List<File> get inputFiles;
109
110 bool wasBuiltOnce();
111
112 bool needsBuild({String manifestPath = defaultManifestPath});
113
114 /// Returns 0 for success; non-zero for failure.
115 Future<int> build({
116 String manifestPath = defaultManifestPath,
117 required String packageConfigPath,
118 bool deferredComponentsEnabled = false,
119 TargetPlatform? targetPlatform,
120 String? flavor,
121 bool includeAssetsFromDevDependencies = false,
122 });
123}
124
125class _ManifestAssetBundleFactory implements AssetBundleFactory {
126 _ManifestAssetBundleFactory({
127 required Logger logger,
128 required FileSystem fileSystem,
129 required Platform platform,
130 bool splitDeferredAssets = false,
131 }) : _logger = logger,
132 _fileSystem = fileSystem,
133 _platform = platform,
134 _splitDeferredAssets = splitDeferredAssets;
135
136 final Logger _logger;
137 final FileSystem _fileSystem;
138 final Platform _platform;
139 final bool _splitDeferredAssets;
140
141 @override
142 AssetBundle createBundle() => ManifestAssetBundle(
143 logger: _logger,
144 fileSystem: _fileSystem,
145 platform: _platform,
146 flutterRoot: Cache.flutterRoot!,
147 splitDeferredAssets: _splitDeferredAssets,
148 );
149}
150
151/// An asset bundle based on a pubspec.yaml file.
152class ManifestAssetBundle implements AssetBundle {
153 /// Constructs an [ManifestAssetBundle] that gathers the set of assets from the
154 /// pubspec.yaml manifest.
155 ManifestAssetBundle({
156 required Logger logger,
157 required FileSystem fileSystem,
158 required Platform platform,
159 required String flutterRoot,
160 bool splitDeferredAssets = false,
161 }) : _logger = logger,
162 _fileSystem = fileSystem,
163 _platform = platform,
164 _flutterRoot = flutterRoot,
165 _splitDeferredAssets = splitDeferredAssets,
166 _licenseCollector = LicenseCollector(fileSystem: fileSystem);
167
168 final Logger _logger;
169 final FileSystem _fileSystem;
170 final LicenseCollector _licenseCollector;
171 final Platform _platform;
172 final String _flutterRoot;
173 final bool _splitDeferredAssets;
174
175 @override
176 final entries = <String, AssetBundleEntry>{};
177
178 @override
179 final deferredComponentsEntries = <String, Map<String, AssetBundleEntry>>{};
180
181 @override
182 final inputFiles = <File>[];
183
184 // If an asset corresponds to a wildcard directory, then it may have been
185 // updated without changes to the manifest. These are only tracked for
186 // the current project.
187 final _wildcardDirectories = <Uri, Directory>{};
188
189 DateTime? _lastBuildTimestamp;
190
191 // We assume the main asset is designed for a device pixel ratio of 1.0.
192 static const _kAssetManifestJsonFilename = 'AssetManifest.json';
193 static const _kAssetManifestBinFilename = 'AssetManifest.bin';
194 static const _kAssetManifestBinJsonFilename = 'AssetManifest.bin.json';
195
196 static const _kNoticeFile = 'NOTICES';
197 // Comically, this can't be name with the more common .gz file extension
198 // because when it's part of an AAR and brought into another APK via gradle,
199 // gradle individually traverses all the files of the AAR and unzips .gz
200 // files (b/37117906). A less common .Z extension still describes how the
201 // file is formatted if users want to manually inspect the application
202 // bundle and is recognized by default file handlers on OS such as macOS.˚
203 static const _kNoticeZippedFile = 'NOTICES.Z';
204
205 @override
206 bool wasBuiltOnce() => _lastBuildTimestamp != null;
207
208 @override
209 bool needsBuild({String manifestPath = defaultManifestPath}) {
210 final DateTime? lastBuildTimestamp = _lastBuildTimestamp;
211 if (lastBuildTimestamp == null) {
212 return true;
213 }
214
215 final FileStat manifestStat = _fileSystem.file(manifestPath).statSync();
216 if (manifestStat.type == FileSystemEntityType.notFound) {
217 return true;
218 }
219
220 for (final Directory directory in _wildcardDirectories.values) {
221 if (!directory.existsSync()) {
222 return true; // directory was deleted.
223 }
224 for (final File file in directory.listSync().whereType<File>()) {
225 final DateTime dateTime = file.statSync().modified;
226 if (dateTime.isAfter(lastBuildTimestamp)) {
227 return true;
228 }
229 }
230 }
231
232 return manifestStat.modified.isAfter(lastBuildTimestamp);
233 }
234
235 @override
236 Future<int> build({
237 String manifestPath = defaultManifestPath,
238 FlutterProject? flutterProject,
239 required String packageConfigPath,
240 bool deferredComponentsEnabled = false,
241 TargetPlatform? targetPlatform,
242 String? flavor,
243 bool includeAssetsFromDevDependencies = false,
244 }) async {
245 if (flutterProject == null) {
246 try {
247 flutterProject = FlutterProject.fromDirectory(_fileSystem.file(manifestPath).parent);
248 } on Exception catch (e) {
249 _logger.printStatus('Error detected in pubspec.yaml:', emphasis: true);
250 _logger.printError('$e');
251 return 1;
252 }
253 }
254
255 final FlutterManifest flutterManifest = flutterProject.manifest;
256 // If the last build time isn't set before this early return, empty pubspecs will
257 // hang on hot reload, as the incremental dill files will never be copied to the
258 // device.
259 _lastBuildTimestamp = DateTime.now();
260 if (flutterManifest.isEmpty) {
261 entries[_kAssetManifestJsonFilename] = AssetBundleEntry(
262 DevFSStringContent('{}'),
263 kind: AssetKind.regular,
264 transformers: const <AssetTransformerEntry>[],
265 );
266 final ByteData emptyAssetManifest = const StandardMessageCodec().encodeMessage(
267 <dynamic, dynamic>{},
268 )!;
269 entries[_kAssetManifestBinFilename] = AssetBundleEntry(
270 DevFSByteContent(
271 emptyAssetManifest.buffer.asUint8List(0, emptyAssetManifest.lengthInBytes),
272 ),
273 kind: AssetKind.regular,
274 transformers: const <AssetTransformerEntry>[],
275 );
276 // Create .bin.json on web builds.
277 if (targetPlatform == TargetPlatform.web_javascript) {
278 entries[_kAssetManifestBinJsonFilename] = AssetBundleEntry(
279 DevFSStringContent('""'),
280 kind: AssetKind.regular,
281 transformers: const <AssetTransformerEntry>[],
282 );
283 }
284 return 0;
285 }
286
287 final String assetBasePath = _fileSystem.path.dirname(_fileSystem.path.absolute(manifestPath));
288 final File packageConfigFile = _fileSystem.file(packageConfigPath);
289 inputFiles.add(packageConfigFile);
290 final PackageConfig packageConfig = await loadPackageConfigWithLogging(
291 packageConfigFile,
292 logger: _logger,
293 );
294 final wildcardDirectories = <Uri>[];
295
296 // The _assetVariants map contains an entry for each asset listed
297 // in the pubspec.yaml file's assets and font sections. The
298 // value of each image asset is a list of resolution-specific "variants",
299 // see _AssetDirectoryCache.
300 final Map<_Asset, List<_Asset>>? assetVariants = _parseAssets(
301 packageConfig,
302 flutterManifest,
303 wildcardDirectories,
304 assetBasePath,
305 targetPlatform,
306 flavor: flavor,
307 );
308
309 if (assetVariants == null) {
310 return 1;
311 }
312
313 // Parse assets for deferred components.
314 final Map<String, Map<_Asset, List<_Asset>>> deferredComponentsAssetVariants =
315 _parseDeferredComponentsAssets(
316 flutterManifest,
317 packageConfig,
318 assetBasePath,
319 wildcardDirectories,
320 flutterProject.directory,
321 flavor: flavor,
322 );
323 if (!_splitDeferredAssets || !deferredComponentsEnabled) {
324 // Include the assets in the regular set of assets if not using deferred
325 // components.
326 deferredComponentsAssetVariants.values.forEach(assetVariants.addAll);
327 deferredComponentsAssetVariants.clear();
328 deferredComponentsEntries.clear();
329 }
330
331 final bool includesMaterialFonts = flutterManifest.usesMaterialDesign;
332 final List<Map<String, Object?>> fonts = _parseFonts(
333 flutterManifest,
334 packageConfig,
335 primary: true,
336 );
337
338 // Add fonts, assets, and licenses from packages in the project's
339 // dependencies.
340 // To avoid bundling assets from dev_dependencies and other pub workspace
341 // packages, we compute the set of transitive dependencies.
342 final List<Dependency> transitiveDependencies = computeTransitiveDependencies(
343 flutterProject,
344 packageConfig,
345 );
346 final additionalLicenseFiles = <String, List<File>>{};
347 for (final dependency in transitiveDependencies) {
348 if (!includeAssetsFromDevDependencies && dependency.isExclusiveDevDependency) {
349 continue;
350 }
351 final String packageName = dependency.name;
352 final Package? package = packageConfig[packageName];
353 if (package == null) {
354 // This can happen with eg. `flutter run --no-pub`.
355 //
356 // We usually expect the package config to be up to date with the
357 // current pubspec.yaml - but because we can force pub get to not be run
358 // with `flutter run --no-pub` we can end up with a new dependency in
359 // pubspec.yaml that is not yet discovered by pub and placed in the
360 // package config.
361 throwToolExit('Could not locate package:$packageName. Try running `flutter pub get`.');
362 }
363 final Uri packageUri = package.packageUriRoot;
364 if (packageUri.scheme == 'file') {
365 final String packageManifestPath = _fileSystem.path.fromUri(
366 packageUri.resolve('../pubspec.yaml'),
367 );
368 inputFiles.add(_fileSystem.file(packageManifestPath));
369 final FlutterManifest? packageFlutterManifest = FlutterManifest.createFromPath(
370 packageManifestPath,
371 logger: _logger,
372 fileSystem: _fileSystem,
373 );
374 if (packageFlutterManifest == null) {
375 continue;
376 }
377 // Collect any additional licenses from each package.
378 final licenseFiles = <File>[];
379 for (final String relativeLicensePath in packageFlutterManifest.additionalLicenses) {
380 final String absoluteLicensePath = _fileSystem.path.fromUri(
381 package.root.resolve(relativeLicensePath),
382 );
383 licenseFiles.add(_fileSystem.file(absoluteLicensePath).absolute);
384 }
385 additionalLicenseFiles[packageFlutterManifest.appName] = licenseFiles;
386
387 // Skip the app itself
388 if (packageFlutterManifest.appName == flutterManifest.appName) {
389 continue;
390 }
391 final String packageBasePath = _fileSystem.path.dirname(packageManifestPath);
392
393 final Map<_Asset, List<_Asset>>? packageAssets = _parseAssets(
394 packageConfig,
395 packageFlutterManifest,
396 // Do not track wildcard directories for dependencies.
397 <Uri>[],
398 packageBasePath,
399 targetPlatform,
400 packageName: package.name,
401 attributedPackage: package,
402 flavor: flavor,
403 );
404
405 if (packageAssets == null) {
406 return 1;
407 }
408 assetVariants.addAll(packageAssets);
409 if (!includesMaterialFonts && packageFlutterManifest.usesMaterialDesign) {
410 _logger.printError(
411 'package:${package.name} has `uses-material-design: true` set but '
412 'the primary pubspec contains `uses-material-design: false`. '
413 'If the application needs material icons, then `uses-material-design` '
414 ' must be set to true.',
415 );
416 }
417 fonts.addAll(
418 _parseFonts(
419 packageFlutterManifest,
420 packageConfig,
421 packageName: package.name,
422 primary: false,
423 ),
424 );
425 }
426 }
427
428 // Save the contents of each image, image variant, and font
429 // asset in entries.
430 for (final _Asset asset in assetVariants.keys) {
431 final File assetFile = asset.lookupAssetFile(_fileSystem);
432 final List<_Asset> variants = assetVariants[asset]!;
433 if (!assetFile.existsSync() && variants.isEmpty) {
434 _logger.printStatus('Error detected in pubspec.yaml:', emphasis: true);
435 _logger.printError('No file or variants found for $asset.\n');
436 if (asset.package != null) {
437 _logger.printError('This asset was included from package ${asset.package?.name}.');
438 }
439 return 1;
440 }
441 // The file name for an asset's "main" entry is whatever appears in
442 // the pubspec.yaml file. The main entry's file must always exist for
443 // font assets. It need not exist for an image if resolution-specific
444 // variant files exist. An image's main entry is treated the same as a
445 // "1x" resolution variant and if both exist then the explicit 1x
446 // variant is preferred.
447 if (assetFile.existsSync() && !variants.contains(asset)) {
448 variants.insert(0, asset);
449 }
450 for (final variant in variants) {
451 final File variantFile = variant.lookupAssetFile(_fileSystem);
452 inputFiles.add(variantFile);
453 assert(variantFile.existsSync());
454 _setIfConfigurationChanged(
455 entries,
456 variant.entryUri.path,
457 AssetBundleEntry(
458 DevFSFileContent(variantFile),
459 kind: variant.kind,
460 transformers: variant.transformers,
461 ),
462 );
463 }
464 }
465 // Save the contents of each deferred component image, image variant, and font
466 // asset in deferredComponentsEntries.
467 for (final String componentName in deferredComponentsAssetVariants.keys) {
468 deferredComponentsEntries[componentName] = <String, AssetBundleEntry>{};
469 final Map<_Asset, List<_Asset>> assetsMap = deferredComponentsAssetVariants[componentName]!;
470 for (final _Asset asset in assetsMap.keys) {
471 final File assetFile = asset.lookupAssetFile(_fileSystem);
472 if (!assetFile.existsSync() && assetsMap[asset]!.isEmpty) {
473 _logger.printStatus('Error detected in pubspec.yaml:', emphasis: true);
474 _logger.printError('No file or variants found for $asset.\n');
475 if (asset.package != null) {
476 _logger.printError('This asset was included from package ${asset.package?.name}.');
477 }
478 return 1;
479 }
480 // The file name for an asset's "main" entry is whatever appears in
481 // the pubspec.yaml file. The main entry's file must always exist for
482 // font assets. It need not exist for an image if resolution-specific
483 // variant files exist. An image's main entry is treated the same as a
484 // "1x" resolution variant and if both exist then the explicit 1x
485 // variant is preferred.
486 if (assetFile.existsSync() && !assetsMap[asset]!.contains(asset)) {
487 assetsMap[asset]!.insert(0, asset);
488 }
489 for (final _Asset variant in assetsMap[asset]!) {
490 final File variantFile = variant.lookupAssetFile(_fileSystem);
491 assert(variantFile.existsSync());
492 _setIfConfigurationChanged(
493 deferredComponentsEntries[componentName]!,
494 variant.entryUri.path,
495 AssetBundleEntry(
496 DevFSFileContent(variantFile),
497 kind: AssetKind.regular,
498 transformers: variant.transformers,
499 ),
500 );
501 }
502 }
503 }
504 final materialAssets = <_Asset>[
505 if (flutterManifest.usesMaterialDesign) ..._getMaterialFonts(),
506 // For all platforms, include the shaders unconditionally. They are
507 // small, and whether they're used is determined only by the app source
508 // code and not by the Flutter manifest.
509 ..._getMaterialShaders(),
510 ];
511 for (final asset in materialAssets) {
512 final File assetFile = asset.lookupAssetFile(_fileSystem);
513 assert(assetFile.existsSync(), 'Missing ${assetFile.path}');
514 entries[asset.entryUri.path] ??= AssetBundleEntry(
515 DevFSFileContent(assetFile),
516 kind: asset.kind,
517 transformers: const <AssetTransformerEntry>[],
518 );
519 }
520
521 // Update wildcard directories we can detect changes in them.
522 for (final uri in wildcardDirectories) {
523 _wildcardDirectories[uri] ??= _fileSystem.directory(uri);
524 }
525
526 final Map<String, List<String>> assetManifest = _createAssetManifest(
527 assetVariants,
528 deferredComponentsAssetVariants,
529 );
530 final DevFSByteContent assetManifestBinary = _createAssetManifestBinary(assetManifest);
531 final assetManifestJson = DevFSStringContent(json.encode(assetManifest));
532 final fontManifest = DevFSStringContent(json.encode(fonts));
533 final LicenseResult licenseResult = _licenseCollector.obtainLicenses(
534 packageConfig,
535 additionalLicenseFiles,
536 );
537 if (licenseResult.errorMessages.isNotEmpty) {
538 licenseResult.errorMessages.forEach(_logger.printError);
539 return 1;
540 }
541
542 additionalDependencies = licenseResult.dependencies;
543 inputFiles.addAll(additionalDependencies);
544
545 if (wildcardDirectories.isNotEmpty) {
546 // Force the depfile to contain missing files so that Gradle does not skip
547 // the task. Wildcard directories are not compatible with full incremental
548 // builds. For more context see https://github.com/flutter/flutter/issues/56466 .
549 _logger.printTrace(
550 'Manifest contained wildcard assets. Inserting missing file into '
551 'build graph to force rerun. for more information see #56466.',
552 );
553 final suffix = Object().hashCode;
554 additionalDependencies.add(
555 _fileSystem.file('DOES_NOT_EXIST_RERUN_FOR_WILDCARD$suffix').absolute,
556 );
557 }
558
559 _setIfChanged(_kAssetManifestJsonFilename, assetManifestJson, AssetKind.regular);
560 _setIfChanged(_kAssetManifestBinFilename, assetManifestBinary, AssetKind.regular);
561 // Create .bin.json on web builds.
562 if (targetPlatform == TargetPlatform.web_javascript) {
563 final assetManifestBinaryJson = DevFSStringContent(
564 json.encode(base64.encode(assetManifestBinary.bytes)),
565 );
566 _setIfChanged(_kAssetManifestBinJsonFilename, assetManifestBinaryJson, AssetKind.regular);
567 }
568 _setIfChanged(kFontManifestJson, fontManifest, AssetKind.regular);
569 _setLicenseIfChanged(licenseResult.combinedLicenses, targetPlatform);
570 return 0;
571 }
572
573 @override
574 var additionalDependencies = <File>[];
575 void _setIfChanged(String key, DevFSContent content, AssetKind assetKind) {
576 final DevFSContent? oldContent = entries[key]?.content;
577 // In the case that the content is unchanged, we want to avoid an overwrite
578 // as the isModified property may be reset to true,
579 if (oldContent is DevFSByteContent &&
580 content is DevFSByteContent &&
581 _compareIntLists(oldContent.bytes, content.bytes)) {
582 return;
583 }
584
585 entries[key] = AssetBundleEntry(
586 content,
587 kind: assetKind,
588 transformers: const <AssetTransformerEntry>[],
589 );
590 }
591
592 static bool _compareIntLists(List<int> o1, List<int> o2) {
593 if (o1.length != o2.length) {
594 return false;
595 }
596
597 for (var index = 0; index < o1.length; index++) {
598 if (o1[index] != o2[index]) {
599 return false;
600 }
601 }
602
603 return true;
604 }
605
606 void _setIfConfigurationChanged(
607 Map<String, AssetBundleEntry> entryMap,
608 String key,
609 AssetBundleEntry entry,
610 ) {
611 final AssetBundleEntry? existingEntry = entryMap[key];
612 if (existingEntry == null || !entry.hasEquivalentConfigurationWith(existingEntry)) {
613 entryMap[key] = entry;
614 }
615 }
616
617 void _setLicenseIfChanged(String combinedLicenses, TargetPlatform? targetPlatform) {
618 // On the web, don't compress the NOTICES file since the client doesn't have
619 // dart:io to decompress it. So use the standard _setIfChanged to check if
620 // the strings still match.
621 if (targetPlatform == TargetPlatform.web_javascript) {
622 _setIfChanged(_kNoticeFile, DevFSStringContent(combinedLicenses), AssetKind.regular);
623 return;
624 }
625
626 // On other platforms, let the NOTICES file be compressed. But use a
627 // specialized DevFSStringCompressingBytesContent class to compare
628 // the uncompressed strings to not incur decompression/decoding while making
629 // the comparison.
630 if (!entries.containsKey(_kNoticeZippedFile) ||
631 (entries[_kNoticeZippedFile]?.content as DevFSStringCompressingBytesContent?)?.equals(
632 combinedLicenses,
633 ) !=
634 true) {
635 entries[_kNoticeZippedFile] = AssetBundleEntry(
636 DevFSStringCompressingBytesContent(
637 combinedLicenses,
638 // A zlib dictionary is a hinting string sequence with the most
639 // likely string occurrences at the end. This ends up just being
640 // common English words with domain specific words like copyright.
641 hintString: 'copyrightsoftwaretothisinandorofthe',
642 ),
643 kind: AssetKind.regular,
644 transformers: const <AssetTransformerEntry>[],
645 );
646 }
647 }
648
649 List<_Asset> _getMaterialFonts() {
650 final result = <_Asset>[];
651 for (final Map<String, Object> family in kMaterialFonts) {
652 final Object? fonts = family['fonts'];
653 if (fonts == null) {
654 continue;
655 }
656 for (final Map<String, Object> font in fonts as List<Map<String, String>>) {
657 final asset = font['asset'] as String?;
658 if (asset == null) {
659 continue;
660 }
661 final Uri entryUri = _fileSystem.path.toUri(asset);
662 result.add(
663 _Asset(
664 baseDir: _fileSystem.path.join(
665 _flutterRoot,
666 'bin',
667 'cache',
668 'artifacts',
669 'material_fonts',
670 ),
671 relativeUri: Uri(path: entryUri.pathSegments.last),
672 entryUri: entryUri,
673 package: null,
674 kind: AssetKind.font,
675 ),
676 );
677 }
678 }
679
680 return result;
681 }
682
683 List<_Asset> _getMaterialShaders() {
684 final String shaderPath = _fileSystem.path.join(
685 _flutterRoot,
686 'packages',
687 'flutter',
688 'lib',
689 'src',
690 'material',
691 'shaders',
692 );
693 // This file will exist in a real invocation unless the git checkout is
694 // corrupted somehow, but unit tests generally don't create this file
695 // in their mock file systems. Leaving it out in those cases is harmless.
696 if (!_fileSystem.directory(shaderPath).existsSync()) {
697 return <_Asset>[];
698 }
699
700 final result = <_Asset>[];
701 for (final String shader in kMaterialShaders) {
702 final Uri entryUri = _fileSystem.path.toUri(shader);
703 result.add(
704 _Asset(
705 baseDir: shaderPath,
706 relativeUri: Uri(path: entryUri.pathSegments.last),
707 entryUri: entryUri,
708 package: null,
709 kind: AssetKind.shader,
710 ),
711 );
712 }
713
714 return result;
715 }
716
717 List<Map<String, Object?>> _parseFonts(
718 FlutterManifest manifest,
719 PackageConfig packageConfig, {
720 String? packageName,
721 required bool primary,
722 }) {
723 return <Map<String, Object?>>[
724 if (primary && manifest.usesMaterialDesign) ...kMaterialFonts,
725 if (packageName == null)
726 ...manifest.fontsDescriptor
727 else
728 for (final Font font in _parsePackageFonts(manifest, packageName, packageConfig))
729 font.descriptor,
730 ];
731 }
732
733 Map<String, Map<_Asset, List<_Asset>>> _parseDeferredComponentsAssets(
734 FlutterManifest flutterManifest,
735 PackageConfig packageConfig,
736 String assetBasePath,
737 List<Uri> wildcardDirectories,
738 Directory projectDirectory, {
739 String? flavor,
740 }) {
741 final List<DeferredComponent>? components = flutterManifest.deferredComponents;
742 final deferredComponentsAssetVariants = <String, Map<_Asset, List<_Asset>>>{};
743 if (components == null) {
744 return deferredComponentsAssetVariants;
745 }
746 for (final DeferredComponent component in components) {
747 final cache = _AssetDirectoryCache(_fileSystem);
748 final componentAssets = <_Asset, List<_Asset>>{};
749 for (final AssetsEntry assetsEntry in component.assets) {
750 if (assetsEntry.uri.path.endsWith('/')) {
751 wildcardDirectories.add(assetsEntry.uri);
752 _parseAssetsFromFolder(
753 packageConfig,
754 flutterManifest,
755 assetBasePath,
756 cache,
757 componentAssets,
758 assetsEntry.uri,
759 flavors: assetsEntry.flavors,
760 transformers: assetsEntry.transformers,
761 );
762 } else {
763 _parseAssetFromFile(
764 packageConfig,
765 flutterManifest,
766 assetBasePath,
767 cache,
768 componentAssets,
769 assetsEntry.uri,
770 flavors: assetsEntry.flavors,
771 transformers: assetsEntry.transformers,
772 );
773 }
774 }
775
776 componentAssets.removeWhere(
777 (_Asset asset, List<_Asset> variants) => !asset.matchesFlavor(flavor),
778 );
779 deferredComponentsAssetVariants[component.name] = componentAssets;
780 }
781 return deferredComponentsAssetVariants;
782 }
783
784 Map<String, List<String>> _createAssetManifest(
785 Map<_Asset, List<_Asset>> assetVariants,
786 Map<String, Map<_Asset, List<_Asset>>> deferredComponentsAssetVariants,
787 ) {
788 final manifest = <String, List<String>>{};
789 final entries = <_Asset, List<String>>{};
790 assetVariants.forEach((_Asset main, List<_Asset> variants) {
791 entries[main] = <String>[for (final _Asset variant in variants) variant.entryUri.path];
792 });
793 for (final Map<_Asset, List<_Asset>> componentAssets
794 in deferredComponentsAssetVariants.values) {
795 componentAssets.forEach((_Asset main, List<_Asset> variants) {
796 entries[main] = <String>[for (final _Asset variant in variants) variant.entryUri.path];
797 });
798 }
799 final List<_Asset> sortedKeys = entries.keys.toList()
800 ..sort((_Asset left, _Asset right) => left.entryUri.path.compareTo(right.entryUri.path));
801 for (final main in sortedKeys) {
802 final String decodedEntryPath = Uri.decodeFull(main.entryUri.path);
803 final List<String> rawEntryVariantsPaths = entries[main]!;
804 final List<String> decodedEntryVariantPaths = rawEntryVariantsPaths
805 .map((String value) => Uri.decodeFull(value))
806 .toList();
807 manifest[decodedEntryPath] = decodedEntryVariantPaths;
808 }
809 return manifest;
810 }
811
812 // Matches path-like strings ending in a number followed by an 'x'.
813 // Example matches include "assets/animals/2.0x", "plants/3x", and "2.7x".
814 static final _extractPixelRatioFromKeyRegExp = RegExp(r'/?(\d+(\.\d*)?)x$');
815
816 DevFSByteContent _createAssetManifestBinary(Map<String, List<String>> assetManifest) {
817 double? parseScale(String key) {
818 final Uri assetUri = Uri.parse(key);
819 var directoryPath = '';
820 if (assetUri.pathSegments.length > 1) {
821 directoryPath = assetUri.pathSegments[assetUri.pathSegments.length - 2];
822 }
823
824 final Match? match = _extractPixelRatioFromKeyRegExp.firstMatch(directoryPath);
825 if (match != null && match.groupCount > 0) {
826 return double.parse(match.group(1)!);
827 }
828
829 return null;
830 }
831
832 final result = <String, dynamic>{};
833
834 for (final MapEntry<String, dynamic> manifestEntry in assetManifest.entries) {
835 final resultVariants = <dynamic>[];
836 final List<String> entries = (manifestEntry.value as List<dynamic>).cast<String>();
837 for (final variant in entries) {
838 final resultVariant = <String, dynamic>{};
839 final double? variantDevicePixelRatio = parseScale(variant);
840 resultVariant['asset'] = variant;
841 if (variantDevicePixelRatio != null) {
842 resultVariant['dpr'] = variantDevicePixelRatio;
843 }
844 resultVariants.add(resultVariant);
845 }
846 result[manifestEntry.key] = resultVariants;
847 }
848
849 final ByteData message = const StandardMessageCodec().encodeMessage(result)!;
850 return DevFSByteContent(message.buffer.asUint8List(0, message.lengthInBytes));
851 }
852
853 /// Prefixes family names and asset paths of fonts included from packages with
854 /// `packages/<package_name>`.
855 List<Font> _parsePackageFonts(
856 FlutterManifest manifest,
857 String packageName,
858 PackageConfig packageConfig,
859 ) {
860 final packageFonts = <Font>[];
861 for (final Font font in manifest.fonts) {
862 final packageFontAssets = <FontAsset>[];
863 for (final FontAsset fontAsset in font.fontAssets) {
864 final Uri assetUri = fontAsset.assetUri;
865 if (assetUri.pathSegments.first == 'packages' &&
866 !_fileSystem.isFileSync(
867 _fileSystem.path.fromUri(
868 packageConfig[packageName]?.packageUriRoot.resolve('../${assetUri.path}'),
869 ),
870 )) {
871 packageFontAssets.add(
872 FontAsset(fontAsset.assetUri, weight: fontAsset.weight, style: fontAsset.style),
873 );
874 } else {
875 packageFontAssets.add(
876 FontAsset(
877 Uri(pathSegments: <String>['packages', packageName, ...assetUri.pathSegments]),
878 weight: fontAsset.weight,
879 style: fontAsset.style,
880 ),
881 );
882 }
883 }
884 packageFonts.add(Font('packages/$packageName/${font.familyName}', packageFontAssets));
885 }
886 return packageFonts;
887 }
888
889 /// Given an assetBase location and a pubspec.yaml Flutter manifest, return a
890 /// map of assets to asset variants.
891 ///
892 /// Returns null on missing assets.
893 ///
894 /// Given package: 'test_package' and an assets directory like this:
895 ///
896 /// - assets/foo
897 /// - assets/var1/foo
898 /// - assets/var2/foo
899 /// - assets/bar
900 ///
901 /// This will return:
902 /// ```none
903 /// {
904 /// asset: packages/test_package/assets/foo: [
905 /// asset: packages/test_package/assets/foo,
906 /// asset: packages/test_package/assets/var1/foo,
907 /// asset: packages/test_package/assets/var2/foo,
908 /// ],
909 /// asset: packages/test_package/assets/bar: [
910 /// asset: packages/test_package/assets/bar,
911 /// ],
912 /// }
913 /// ```
914 Map<_Asset, List<_Asset>>? _parseAssets(
915 PackageConfig packageConfig,
916 FlutterManifest flutterManifest,
917 List<Uri> wildcardDirectories,
918 String assetBase,
919 TargetPlatform? targetPlatform, {
920 String? packageName,
921 Package? attributedPackage,
922 required String? flavor,
923 }) {
924 final result = <_Asset, List<_Asset>>{};
925
926 final cache = _AssetDirectoryCache(_fileSystem);
927 for (final AssetsEntry assetsEntry in flutterManifest.assets) {
928 if (assetsEntry.uri.path.endsWith('/')) {
929 wildcardDirectories.add(assetsEntry.uri);
930 _parseAssetsFromFolder(
931 packageConfig,
932 flutterManifest,
933 assetBase,
934 cache,
935 result,
936 assetsEntry.uri,
937 packageName: packageName,
938 attributedPackage: attributedPackage,
939 flavors: assetsEntry.flavors,
940 transformers: assetsEntry.transformers,
941 );
942 } else {
943 _parseAssetFromFile(
944 packageConfig,
945 flutterManifest,
946 assetBase,
947 cache,
948 result,
949 assetsEntry.uri,
950 packageName: packageName,
951 attributedPackage: attributedPackage,
952 flavors: assetsEntry.flavors,
953 transformers: assetsEntry.transformers,
954 );
955 }
956 }
957
958 result.removeWhere((_Asset asset, List<_Asset> variants) {
959 if (!asset.matchesFlavor(flavor)) {
960 _logger.printTrace(
961 'Skipping assets entry "${asset.entryUri.path}" since '
962 'its configured flavor(s) did not match the provided flavor (if any).\n'
963 'Configured flavors: ${asset.flavors.join(', ')}\n',
964 );
965 return true;
966 }
967 return false;
968 });
969
970 for (final Uri shaderUri in flutterManifest.shaders) {
971 _parseAssetFromFile(
972 packageConfig,
973 flutterManifest,
974 assetBase,
975 cache,
976 result,
977 shaderUri,
978 packageName: packageName,
979 attributedPackage: attributedPackage,
980 assetKind: AssetKind.shader,
981 flavors: <String>{},
982 transformers: <AssetTransformerEntry>[],
983 );
984 }
985
986 // Add assets referenced in the fonts section of the manifest.
987 for (final Font font in flutterManifest.fonts) {
988 for (final FontAsset fontAsset in font.fontAssets) {
989 final _Asset baseAsset = _resolveAsset(
990 packageConfig,
991 assetBase,
992 fontAsset.assetUri,
993 packageName,
994 attributedPackage,
995 assetKind: AssetKind.font,
996 flavors: <String>{},
997 transformers: <AssetTransformerEntry>[],
998 );
999 final File baseAssetFile = baseAsset.lookupAssetFile(_fileSystem);
1000 if (!baseAssetFile.existsSync()) {
1001 _logger.printError(
1002 'Error: unable to locate asset entry in pubspec.yaml: "${fontAsset.assetUri}".',
1003 );
1004 return null;
1005 }
1006 result[baseAsset] = <_Asset>[];
1007 }
1008 }
1009
1010 return result;
1011 }
1012
1013 void _parseAssetsFromFolder(
1014 PackageConfig packageConfig,
1015 FlutterManifest flutterManifest,
1016 String assetBase,
1017 _AssetDirectoryCache cache,
1018 Map<_Asset, List<_Asset>> result,
1019 Uri assetUri, {
1020 String? packageName,
1021 Package? attributedPackage,
1022 required Set<String> flavors,
1023 required List<AssetTransformerEntry> transformers,
1024 }) {
1025 final String directoryPath;
1026 try {
1027 directoryPath = _fileSystem.path.join(
1028 assetBase,
1029 assetUri.toFilePath(windows: _platform.isWindows),
1030 );
1031 } on UnsupportedError catch (e) {
1032 throwToolExit(
1033 'Unable to search for asset files in directory path "${assetUri.path}". '
1034 'Please ensure that this entry in pubspec.yaml is a valid file path.\n'
1035 'Error details:\n$e',
1036 );
1037 }
1038
1039 if (!_fileSystem.directory(directoryPath).existsSync()) {
1040 _logger.printError('Error: unable to find directory entry in pubspec.yaml: $directoryPath');
1041 return;
1042 }
1043
1044 final Iterable<FileSystemEntity> entities = _fileSystem.directory(directoryPath).listSync();
1045
1046 final Iterable<File> files = entities.whereType<File>();
1047 for (final file in files) {
1048 final String relativePath = _fileSystem.path.relative(file.path, from: assetBase);
1049 final uri = Uri.file(relativePath, windows: _platform.isWindows);
1050
1051 _parseAssetFromFile(
1052 packageConfig,
1053 flutterManifest,
1054 assetBase,
1055 cache,
1056 result,
1057 uri,
1058 packageName: packageName,
1059 attributedPackage: attributedPackage,
1060 originUri: assetUri,
1061 flavors: flavors,
1062 transformers: transformers,
1063 );
1064 }
1065 }
1066
1067 void _parseAssetFromFile(
1068 PackageConfig packageConfig,
1069 FlutterManifest flutterManifest,
1070 String assetBase,
1071 _AssetDirectoryCache cache,
1072 Map<_Asset, List<_Asset>> result,
1073 Uri assetUri, {
1074 Uri? originUri,
1075 String? packageName,
1076 Package? attributedPackage,
1077 AssetKind assetKind = AssetKind.regular,
1078 required Set<String> flavors,
1079 required List<AssetTransformerEntry> transformers,
1080 }) {
1081 final _Asset asset = _resolveAsset(
1082 packageConfig,
1083 assetBase,
1084 assetUri,
1085 packageName,
1086 attributedPackage,
1087 assetKind: assetKind,
1088 originUri: originUri,
1089 flavors: flavors,
1090 transformers: transformers,
1091 );
1092
1093 _checkForFlavorConflicts(asset, result.keys.toList());
1094
1095 final variants = <_Asset>[];
1096 final File assetFile = asset.lookupAssetFile(_fileSystem);
1097
1098 for (final String path in cache.variantsFor(assetFile.path)) {
1099 final String relativePath = _fileSystem.path.relative(path, from: asset.baseDir);
1100 final Uri relativeUri = _fileSystem.path.toUri(relativePath);
1101 final Uri? entryUri = asset.symbolicPrefixUri == null
1102 ? relativeUri
1103 : asset.symbolicPrefixUri?.resolveUri(relativeUri);
1104 if (entryUri != null) {
1105 variants.add(
1106 _Asset(
1107 baseDir: asset.baseDir,
1108 entryUri: entryUri,
1109 relativeUri: relativeUri,
1110 package: attributedPackage,
1111 kind: assetKind,
1112 flavors: flavors,
1113 transformers: transformers,
1114 ),
1115 );
1116 }
1117 }
1118
1119 result[asset] = variants;
1120 }
1121
1122 // Since it is not clear how overlapping asset declarations should work in the
1123 // presence of conditions such as `flavor`, we throw an Error.
1124 //
1125 // To be more specific, it is not clear if conditions should be combined with
1126 // or-logic or and-logic, or if it should depend on the specificity of the
1127 // declarations (file versus directory). If you would like examples, consider these:
1128 //
1129 // ```yaml
1130 // # Should assets/free.mp3 always be included since "assets/" has no flavor?
1131 // assets:
1132 // - assets/
1133 // - path: assets/free.mp3
1134 // flavor: free
1135 //
1136 // # Should "assets/paid/pip.mp3" be included for both the "paid" and "free" flavors?
1137 // # Or, since "assets/paid/pip.mp3" is more specific than "assets/paid/"", should
1138 // # it take precedence over the latter (included only in "free" flavor)?
1139 // assets:
1140 // - path: assets/paid/
1141 // flavor: paid
1142 // - path: assets/paid/pip.mp3
1143 // flavor: free
1144 // - asset
1145 // ```
1146 //
1147 // Since it is not obvious what logic (if any) would be intuitive and preferable
1148 // to the vast majority of users (if any), we play it safe by throwing a `ToolExit`
1149 // in any of these situations. We can always loosen up this restriction later
1150 // without breaking anyone.
1151 void _checkForFlavorConflicts(_Asset newAsset, List<_Asset> previouslyParsedAssets) {
1152 bool cameFromDirectoryEntry(_Asset asset) {
1153 return asset.originUri.path.endsWith('/');
1154 }
1155
1156 String flavorErrorInfo(_Asset asset) {
1157 if (asset.flavors.isEmpty) {
1158 return 'An entry with the path "${asset.originUri}" does not specify any flavors.';
1159 }
1160
1161 final Iterable<String> flavorsWrappedWithQuotes = asset.flavors.map((String e) => '"$e"');
1162 return 'An entry with the path "${asset.originUri}" specifies the flavor(s): '
1163 '${flavorsWrappedWithQuotes.join(', ')}.';
1164 }
1165
1166 final _Asset? preExistingAsset = previouslyParsedAssets
1167 .where((_Asset other) => other.entryUri == newAsset.entryUri)
1168 .firstOrNull;
1169
1170 if (preExistingAsset == null || preExistingAsset.hasEquivalentFlavorsWith(newAsset)) {
1171 return;
1172 }
1173
1174 final errorMessage = StringBuffer(
1175 'Multiple assets entries include the file '
1176 '"${newAsset.entryUri.path}", but they specify different lists of flavors.\n',
1177 );
1178
1179 errorMessage.writeln(flavorErrorInfo(preExistingAsset));
1180 errorMessage.writeln(flavorErrorInfo(newAsset));
1181
1182 if (cameFromDirectoryEntry(newAsset) || cameFromDirectoryEntry(preExistingAsset)) {
1183 errorMessage.writeln();
1184 errorMessage.write(
1185 'Consider organizing assets with different flavors '
1186 'into different directories.',
1187 );
1188 }
1189
1190 throwToolExit(errorMessage.toString());
1191 }
1192
1193 _Asset _resolveAsset(
1194 PackageConfig packageConfig,
1195 String assetsBaseDir,
1196 Uri assetUri,
1197 String? packageName,
1198 Package? attributedPackage, {
1199 Uri? originUri,
1200 AssetKind assetKind = AssetKind.regular,
1201 required Set<String> flavors,
1202 required List<AssetTransformerEntry> transformers,
1203 }) {
1204 final String assetPath = _fileSystem.path.fromUri(assetUri);
1205 if (assetUri.pathSegments.first == 'packages' &&
1206 !_fileSystem.isFileSync(_fileSystem.path.join(assetsBaseDir, assetPath))) {
1207 // The asset is referenced in the pubspec.yaml as
1208 // 'packages/PACKAGE_NAME/PATH/TO/ASSET .
1209 final _Asset? packageAsset = _resolvePackageAsset(
1210 assetUri,
1211 packageConfig,
1212 attributedPackage,
1213 assetKind: assetKind,
1214 originUri: originUri,
1215 flavors: flavors,
1216 transformers: transformers,
1217 );
1218 if (packageAsset != null) {
1219 return packageAsset;
1220 }
1221 }
1222
1223 return _Asset(
1224 baseDir: assetsBaseDir,
1225 entryUri: packageName == null
1226 ? assetUri // Asset from the current application.
1227 : Uri(
1228 pathSegments: <String>['packages', packageName, ...assetUri.pathSegments],
1229 ), // Asset from, and declared in $packageName.
1230 relativeUri: assetUri,
1231 package: attributedPackage,
1232 originUri: originUri,
1233 kind: assetKind,
1234 flavors: flavors,
1235 transformers: transformers,
1236 );
1237 }
1238
1239 _Asset? _resolvePackageAsset(
1240 Uri assetUri,
1241 PackageConfig packageConfig,
1242 Package? attributedPackage, {
1243 AssetKind assetKind = AssetKind.regular,
1244 Uri? originUri,
1245 Set<String>? flavors,
1246 List<AssetTransformerEntry>? transformers,
1247 }) {
1248 assert(assetUri.pathSegments.first == 'packages');
1249 if (assetUri.pathSegments.length > 1) {
1250 final String packageName = assetUri.pathSegments[1];
1251 final Package? package = packageConfig[packageName];
1252 final Uri? packageUri = package?.packageUriRoot;
1253 if (packageUri != null && packageUri.scheme == 'file') {
1254 return _Asset(
1255 baseDir: _fileSystem.path.fromUri(packageUri),
1256 entryUri: assetUri,
1257 relativeUri: Uri(pathSegments: assetUri.pathSegments.sublist(2)),
1258 package: attributedPackage,
1259 kind: assetKind,
1260 originUri: originUri,
1261 flavors: flavors,
1262 transformers: transformers,
1263 );
1264 }
1265 }
1266 _logger.printStatus('Error detected in pubspec.yaml:', emphasis: true);
1267 _logger.printError('Could not resolve package for asset $assetUri.\n');
1268 if (attributedPackage != null) {
1269 _logger.printError('This asset was included from package ${attributedPackage.name}');
1270 }
1271 return null;
1272 }
1273}
1274
1275@immutable
1276class _Asset {
1277 const _Asset({
1278 required this.baseDir,
1279 Uri? originUri,
1280 required this.relativeUri,
1281 required this.entryUri,
1282 required this.package,
1283 this.kind = AssetKind.regular,
1284 Set<String>? flavors,
1285 List<AssetTransformerEntry>? transformers,
1286 }) : originUri = originUri ?? entryUri,
1287 flavors = flavors ?? const <String>{},
1288 transformers = transformers ?? const <AssetTransformerEntry>[];
1289
1290 final String baseDir;
1291
1292 final Package? package;
1293
1294 /// The platform-independent URL provided by the user in the pubspec that this
1295 /// asset was found from.
1296 final Uri originUri;
1297
1298 /// A platform-independent URL where this asset can be found on disk on the
1299 /// host system relative to [baseDir].
1300 final Uri relativeUri;
1301
1302 /// A platform-independent URL representing the entry for the asset manifest.
1303 final Uri entryUri;
1304
1305 final AssetKind kind;
1306
1307 final Set<String> flavors;
1308
1309 final List<AssetTransformerEntry> transformers;
1310
1311 File lookupAssetFile(FileSystem fileSystem) {
1312 return fileSystem.file(fileSystem.path.join(baseDir, fileSystem.path.fromUri(relativeUri)));
1313 }
1314
1315 /// The delta between what the entryUri is and the relativeUri (e.g.,
1316 /// packages/flutter_gallery).
1317 Uri? get symbolicPrefixUri {
1318 if (entryUri == relativeUri) {
1319 return null;
1320 }
1321 final int index = entryUri.path.indexOf(relativeUri.path);
1322 return index == -1 ? null : Uri(path: entryUri.path.substring(0, index));
1323 }
1324
1325 bool matchesFlavor(String? flavor) {
1326 if (flavors.isEmpty) {
1327 return true;
1328 }
1329
1330 if (flavor == null) {
1331 return false;
1332 }
1333
1334 return flavors.contains(flavor);
1335 }
1336
1337 bool hasEquivalentFlavorsWith(_Asset other) {
1338 final Set<String> assetFlavors = flavors.toSet();
1339 final Set<String> otherFlavors = other.flavors.toSet();
1340 return assetFlavors.length == otherFlavors.length &&
1341 assetFlavors.every((String e) => otherFlavors.contains(e));
1342 }
1343
1344 @override
1345 String toString() => 'asset: $entryUri';
1346
1347 @override
1348 bool operator ==(Object other) {
1349 if (identical(other, this)) {
1350 return true;
1351 }
1352 if (other.runtimeType != runtimeType) {
1353 return false;
1354 }
1355 return other is _Asset &&
1356 other.baseDir == baseDir &&
1357 other.relativeUri == relativeUri &&
1358 other.entryUri == entryUri &&
1359 other.kind == kind &&
1360 hasEquivalentFlavorsWith(other);
1361 }
1362
1363 @override
1364 int get hashCode => Object.hashAll(<Object>[baseDir, relativeUri, entryUri, kind, ...flavors]);
1365}
1366
1367// Given an assets directory like this:
1368//
1369// assets/foo.png
1370// assets/2x/foo.png
1371// assets/3.0x/foo.png
1372// assets/bar/foo.png
1373// assets/bar.png
1374//
1375// variantsFor('assets/foo.png') => ['/assets/foo.png', '/assets/2x/foo.png', 'assets/3.0x/foo.png']
1376// variantsFor('assets/bar.png') => ['/assets/bar.png']
1377// variantsFor('assets/bar/foo.png') => ['/assets/bar/foo.png']
1378class _AssetDirectoryCache {
1379 _AssetDirectoryCache(this._fileSystem);
1380
1381 final FileSystem _fileSystem;
1382 final _cache = <String, List<String>>{};
1383 final _variantsPerFolder = <String, List<File>>{};
1384
1385 List<String> variantsFor(String assetPath) {
1386 final String directoryName = _fileSystem.path.dirname(assetPath);
1387
1388 try {
1389 if (!_fileSystem.directory(directoryName).existsSync()) {
1390 return const <String>[];
1391 }
1392 } on FileSystemException catch (e) {
1393 throwToolExit(
1394 'Unable to check the existence of asset file "$assetPath". '
1395 'Ensure that the asset file is declared as a valid local file system path.\n'
1396 'Details: $e',
1397 );
1398 }
1399
1400 if (_cache.containsKey(assetPath)) {
1401 return _cache[assetPath]!;
1402 }
1403 if (!_variantsPerFolder.containsKey(directoryName)) {
1404 _variantsPerFolder[directoryName] = _fileSystem
1405 .directory(directoryName)
1406 .listSync()
1407 .whereType<Directory>()
1408 .where((Directory dir) => _assetVariantDirectoryRegExp.hasMatch(dir.basename))
1409 .expand((Directory dir) => dir.listSync())
1410 .whereType<File>()
1411 .toList();
1412 }
1413 final File assetFile = _fileSystem.file(assetPath);
1414 final List<File> potentialVariants = _variantsPerFolder[directoryName]!;
1415 final String basename = assetFile.basename;
1416 return _cache[assetPath] = <String>[
1417 // It's possible that the user specifies only explicit variants (e.g. .../1x/asset.png),
1418 // so there does not necessarily need to be a file at the given path.
1419 if (assetFile.existsSync()) assetPath,
1420 ...potentialVariants
1421 .where((File file) => file.basename == basename)
1422 .map((File file) => file.path),
1423 ];
1424 }
1425}
1426