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 | |
5 | import 'dart:typed_data'; |
6 | |
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/common.dart'; |
12 | import 'base/context.dart'; |
13 | import 'base/deferred_component.dart'; |
14 | import 'base/file_system.dart'; |
15 | import 'base/logger.dart'; |
16 | import 'base/platform.dart'; |
17 | import 'base/utils.dart'; |
18 | import 'build_info.dart'; |
19 | import 'cache.dart'; |
20 | import 'convert.dart'; |
21 | import 'dart/package_map.dart'; |
22 | import 'devfs.dart'; |
23 | import 'flutter_manifest.dart'; |
24 | import 'license_collector.dart'; |
25 | import 'package_graph.dart'; |
26 | import 'project.dart'; |
27 | |
28 | const defaultManifestPath = 'pubspec.yaml' ; |
29 | |
30 | const kFontManifestJson = 'FontManifest.json' ; |
31 | |
32 | // Should match '2x', '/1x', '1.5x', etc. |
33 | final _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 | /// ``` |
44 | const 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 | |
53 | const kMaterialShaders = <String>['shaders/ink_sparkle.frag' ]; |
54 | |
55 | /// Injected factory class for spawning [AssetBundle] instances. |
56 | abstract 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 | |
76 | enum 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. |
80 | final 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 | |
94 | abstract 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 | |
125 | class _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. |
152 | class 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 |
1276 | class _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'] |
1378 | class _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 | |