| // Copyright 2014 The Flutter Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| import 'dart:typed_data'; | |
| import 'package:meta/meta.dart'; | |
| import 'package:package_config/package_config.dart'; | |
| import 'package:standard_message_codec/standard_message_codec.dart'; | |
| import 'base/common.dart'; | |
| import 'base/context.dart'; | |
| import 'base/deferred_component.dart'; | |
| import 'base/file_system.dart'; | |
| import 'base/logger.dart'; | |
| import 'base/platform.dart'; | |
| import 'base/utils.dart'; | |
| import 'build_info.dart'; | |
| import 'cache.dart'; | |
| import 'convert.dart'; | |
| import 'dart/package_map.dart'; | |
| import 'devfs.dart'; | |
| import 'flutter_manifest.dart'; | |
| import 'license_collector.dart'; | |
| import 'package_graph.dart'; | |
| import 'project.dart'; | |
| class FlutterHookResult { | |
| const FlutterHookResult({ | |
| required this.buildStart, | |
| required this.buildEnd, | |
| required this.dataAssets, | |
| required this.dependencies, | |
| }); | |
| FlutterHookResult.empty() | |
| : this( | |
| buildStart: DateTime.fromMillisecondsSinceEpoch(0), | |
| buildEnd: DateTime.fromMillisecondsSinceEpoch(0), | |
| dataAssets: <HookAsset>[], | |
| dependencies: <Uri>[], | |
| ); | |
| final List<HookAsset> dataAssets; | |
| /// The timestamp at which we start a build - so the timestamp of the inputs. | |
| final DateTime buildStart; | |
| /// The timestamp at which we finish a build - so the timestamp of the | |
| /// outputs. | |
| final DateTime buildEnd; | |
| /// The dependencies of the build are used to check if the build needs to be | |
| /// rerun. | |
| final List<Uri> dependencies; | |
| /// Whether caller may need to re-run the Dart build. | |
| bool hasAnyModifiedFiles(FileSystem fileSystem) => | |
| _wasAnyFileModifiedSince(fileSystem, buildStart, dependencies); | |
| /// Whether the files produced by the build are up-to-date. | |
| /// | |
| /// NOTICE: The build itself may be up-to-date but the output may not be (as | |
| /// the output may be existing on disc and not be produced by the build | |
| /// itself - in which case we may not need to re-build if the file changes, | |
| /// but we may need to make a new asset bundle with the modified file). | |
| bool isOutputDirty(FileSystem fileSystem) => _wasAnyFileModifiedSince( | |
| fileSystem, | |
| buildEnd, | |
| dataAssets.map((HookAsset e) => e.file).toList(), | |
| ); | |
| static bool _wasAnyFileModifiedSince(FileSystem fileSystem, DateTime since, List<Uri> uris) { | |
| for (final uri in uris) { | |
| final DateTime modified = fileSystem.statSync(uri.toFilePath()).modified; | |
| if (modified.isAfter(since)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| @override | |
| String toString() { | |
| return dataAssets.toString(); | |
| } | |
| } | |
| /// A convenience class to wrap native assets | |
| /// | |
| /// When translating from a `DartHooksResult` to a [FlutterHookResult], where we | |
| /// need to have different classes to not import `isolated/` stuff. | |
| class HookAsset { | |
| HookAsset({required this.file, required this.name, required this.package}); | |
| final Uri file; | |
| final String name; | |
| final String package; | |
| @override | |
| String toString() { | |
| return 'HookAsset(file: $file, name: $name, package: $package)'; | |
| } | |
| } | |
| const defaultManifestPath = 'pubspec.yaml'; | |
| const kFontManifestJson = 'FontManifest.json'; | |
| // Should match '2x', '/1x', '1.5x', etc. | |
| final _assetVariantDirectoryRegExp = RegExp(r'/?(\d+(\.\d*)?)x$'); | |
| /// The effect of adding `uses-material-design: true` to the pubspec is to insert | |
| /// the following snippet into the asset manifest: | |
| /// | |
| /// ```yaml | |
| /// material: | |
| /// - family: MaterialIcons | |
| /// fonts: | |
| /// - asset: fonts/MaterialIcons-Regular.otf | |
| /// ``` | |
| const kMaterialFonts = <Map<String, Object>>[ | |
| <String, Object>{ | |
| 'family': 'MaterialIcons', | |
| 'fonts': <Map<String, String>>[ | |
| <String, String>{'asset': 'fonts/MaterialIcons-Regular.otf'}, | |
| ], | |
| }, | |
| ]; | |
| /// Injected factory class for spawning [AssetBundle] instances. | |
| abstract class AssetBundleFactory { | |
| /// The singleton instance, pulled from the [AppContext]. | |
| static AssetBundleFactory get instance => context.get<AssetBundleFactory>()!; | |
| static AssetBundleFactory defaultInstance({ | |
| required Logger logger, | |
| required FileSystem fileSystem, | |
| required Platform platform, | |
| bool splitDeferredAssets = false, | |
| }) => _ManifestAssetBundleFactory( | |
| logger: logger, | |
| fileSystem: fileSystem, | |
| platform: platform, | |
| splitDeferredAssets: splitDeferredAssets, | |
| ); | |
| /// Creates a new [AssetBundle]. | |
| AssetBundle createBundle(); | |
| } | |
| enum AssetKind { regular, font, shader } | |
| /// Contains all information about an asset needed by tool the to prepare and | |
| /// copy an asset file to the build output. | |
| final class AssetBundleEntry { | |
| AssetBundleEntry(this.content, {required this.kind, required this.transformers}); | |
| final DevFSContent content; | |
| final AssetKind kind; | |
| final List<AssetTransformerEntry> transformers; | |
| Future<List<int>> contentsAsBytes() => content.contentsAsBytes(); | |
| bool hasEquivalentConfigurationWith(AssetBundleEntry other) { | |
| return listEquals(transformers, other.transformers); | |
| } | |
| } | |
| abstract class AssetBundle { | |
| /// The files that were specified under the `assets` section in the pubspec, | |
| /// indexed by asset key. | |
| Map<String, AssetBundleEntry> get entries; | |
| /// The files that were specified under the deferred components assets sections | |
| /// in a pubspec, indexed by component name and asset key. | |
| Map<String, Map<String, AssetBundleEntry>> get deferredComponentsEntries; | |
| /// Additional files that this bundle depends on that are not included in the | |
| /// output result. | |
| List<File> get additionalDependencies; | |
| /// Input files used to build this asset bundle. | |
| List<File> get inputFiles; | |
| bool wasBuiltOnce(); | |
| bool needsBuild({String manifestPath = defaultManifestPath}); | |
| /// Returns 0 for success; non-zero for failure. | |
| Future<int> build({ | |
| FlutterHookResult? flutterHookResult, | |
| String manifestPath = defaultManifestPath, | |
| required String packageConfigPath, | |
| bool deferredComponentsEnabled = false, | |
| required TargetPlatform targetPlatform, | |
| String? flavor, | |
| bool includeAssetsFromDevDependencies = false, | |
| }); | |
| } | |
| class _ManifestAssetBundleFactory implements AssetBundleFactory { | |
| _ManifestAssetBundleFactory({ | |
| required this._logger, | |
| required this._fileSystem, | |
| required this._platform, | |
| this._splitDeferredAssets = false, | |
| }); | |
| final Logger _logger; | |
| final FileSystem _fileSystem; | |
| final Platform _platform; | |
| final bool _splitDeferredAssets; | |
| @override | |
| AssetBundle createBundle() => ManifestAssetBundle( | |
| logger: _logger, | |
| fileSystem: _fileSystem, | |
| platform: _platform, | |
| flutterRoot: Cache.flutterRoot!, | |
| splitDeferredAssets: _splitDeferredAssets, | |
| ); | |
| } | |
| /// An asset bundle based on a pubspec.yaml file. | |
| class ManifestAssetBundle implements AssetBundle { | |
| /// Constructs an [ManifestAssetBundle] that gathers the set of assets from the | |
| /// pubspec.yaml manifest. | |
| ManifestAssetBundle({ | |
| required this._logger, | |
| required FileSystem fileSystem, | |
| required this._platform, | |
| required this._flutterRoot, | |
| this._splitDeferredAssets = false, | |
| }) : _fileSystem = fileSystem, | |
| _licenseCollector = LicenseCollector(fileSystem: fileSystem), | |
| _lastHookResult = FlutterHookResult.empty(); | |
| final Logger _logger; | |
| final FileSystem _fileSystem; | |
| final LicenseCollector _licenseCollector; | |
| final Platform _platform; | |
| final String _flutterRoot; | |
| final bool _splitDeferredAssets; | |
| @override | |
| final entries = <String, AssetBundleEntry>{}; | |
| @override | |
| final deferredComponentsEntries = <String, Map<String, AssetBundleEntry>>{}; | |
| @override | |
| final inputFiles = <File>[]; | |
| // If an asset corresponds to a wildcard directory, then it may have been | |
| // updated without changes to the manifest. These are only tracked for | |
| // the current project. | |
| final _wildcardDirectories = <Uri, Directory>{}; | |
| DateTime? _lastBuildTimestamp; | |
| FlutterHookResult _lastHookResult; | |
| // We assume the main asset is designed for a device pixel ratio of 1.0. | |
| static const _kAssetManifestBinFilename = 'AssetManifest.bin'; | |
| static const _kAssetManifestBinJsonFilename = 'AssetManifest.bin.json'; | |
| static const _kNoticeFile = 'NOTICES'; | |
| // Comically, this can't be name with the more common .gz file extension | |
| // because when it's part of an AAR and brought into another APK via gradle, | |
| // gradle individually traverses all the files of the AAR and unzips .gz | |
| // files (b/37117906). A less common .Z extension still describes how the | |
| // file is formatted if users want to manually inspect the application | |
| // bundle and is recognized by default file handlers on OS such as macOS.Ëš | |
| static const _kNoticeZippedFile = 'NOTICES.Z'; | |
| @override | |
| bool wasBuiltOnce() => _lastBuildTimestamp != null; | |
| @override | |
| bool needsBuild({String manifestPath = defaultManifestPath}) { | |
| if (!wasBuiltOnce() || | |
| // We need to re-run the Dart build. | |
| _lastHookResult.hasAnyModifiedFiles(_fileSystem) || | |
| // We don't have to re-run the Dart build, but some files the Dart build | |
| // wants us to bundle have changed contents. | |
| _lastHookResult.isOutputDirty(_fileSystem)) { | |
| return true; | |
| } | |
| final DateTime lastBuildTimestamp = _lastBuildTimestamp!; | |
| final FileStat manifestStat = _fileSystem.file(manifestPath).statSync(); | |
| if (manifestStat.type == FileSystemEntityType.notFound || | |
| manifestStat.modified.isAfter(lastBuildTimestamp)) { | |
| return true; | |
| } | |
| for (final Directory directory in _wildcardDirectories.values) { | |
| if (!directory.existsSync()) { | |
| return true; // directory was deleted. | |
| } | |
| for (final File file in directory.listSync().whereType<File>()) { | |
| final DateTime lastModified = file.statSync().modified; | |
| if (lastModified.isAfter(lastBuildTimestamp)) { | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| @override | |
| Future<int> build({ | |
| FlutterHookResult? flutterHookResult, | |
| String manifestPath = defaultManifestPath, | |
| FlutterProject? flutterProject, | |
| required String packageConfigPath, | |
| bool deferredComponentsEnabled = false, | |
| required TargetPlatform targetPlatform, | |
| String? flavor, | |
| bool includeAssetsFromDevDependencies = false, | |
| }) async { | |
| if (flutterProject == null) { | |
| try { | |
| flutterProject = FlutterProject.fromDirectory(_fileSystem.file(manifestPath).parent); | |
| } on Exception catch (e) { | |
| _logger.printStatus('Error detected in pubspec.yaml:', emphasis: true); | |
| _logger.printError('$e'); | |
| return 1; | |
| } | |
| } | |
| final FlutterManifest flutterManifest = flutterProject.manifest; | |
| // If the last build time isn't set before this early return, empty pubspecs will | |
| // hang on hot reload, as the incremental dill files will never be copied to the | |
| // device. | |
| _lastBuildTimestamp = DateTime.now(); | |
| _lastHookResult = flutterHookResult ?? FlutterHookResult.empty(); | |
| if (flutterManifest.isEmpty && (flutterHookResult?.dataAssets.isEmpty ?? true)) { | |
| final ByteData emptyAssetManifest = const StandardMessageCodec().encodeMessage( | |
| <dynamic, dynamic>{}, | |
| )!; | |
| entries[_kAssetManifestBinFilename] = AssetBundleEntry( | |
| DevFSByteContent( | |
| emptyAssetManifest.buffer.asUint8List(0, emptyAssetManifest.lengthInBytes), | |
| ), | |
| kind: AssetKind.regular, | |
| transformers: const <AssetTransformerEntry>[], | |
| ); | |
| // Create .bin.json on web builds. | |
| if (targetPlatform == TargetPlatform.web_javascript) { | |
| entries[_kAssetManifestBinJsonFilename] = AssetBundleEntry( | |
| DevFSStringContent('""'), | |
| kind: AssetKind.regular, | |
| transformers: const <AssetTransformerEntry>[], | |
| ); | |
| } | |
| return 0; | |
| } | |
| final String assetBasePath = _fileSystem.path.dirname(_fileSystem.path.absolute(manifestPath)); | |
| final File packageConfigFile = _fileSystem.file(packageConfigPath); | |
| inputFiles.add(packageConfigFile); | |
| final PackageConfig packageConfig = await loadPackageConfigWithLogging( | |
| packageConfigFile, | |
| logger: _logger, | |
| ); | |
| final wildcardDirectories = <Uri>[]; | |
| // The _assetVariants map contains an entry for each asset listed | |
| // in the pubspec.yaml file's assets and font sections. The | |
| // value of each image asset is a list of resolution-specific "variants", | |
| // see _AssetDirectoryCache. | |
| final Map<_Asset, List<_Asset>>? assetVariants = _parseAssets( | |
| packageConfig, | |
| flutterManifest, | |
| wildcardDirectories, | |
| assetBasePath, | |
| targetPlatform, | |
| flavor: flavor, | |
| ); | |
| if (assetVariants == null) { | |
| return 1; | |
| } | |
| // Parse assets for deferred components. | |
| final Map<String, Map<_Asset, List<_Asset>>> deferredComponentsAssetVariants = | |
| _parseDeferredComponentsAssets( | |
| flutterManifest, | |
| packageConfig, | |
| assetBasePath, | |
| wildcardDirectories, | |
| flutterProject.directory, | |
| targetPlatform: targetPlatform, | |
| flavor: flavor, | |
| ); | |
| if (!_splitDeferredAssets || !deferredComponentsEnabled) { | |
| // Include the assets in the regular set of assets if not using deferred | |
| // components. | |
| deferredComponentsAssetVariants.values.forEach(assetVariants.addAll); | |
| deferredComponentsAssetVariants.clear(); | |
| deferredComponentsEntries.clear(); | |
| } | |
| final bool includesMaterialFonts = flutterManifest.usesMaterialDesign; | |
| final List<Map<String, Object?>> fonts = _parseFonts( | |
| flutterManifest, | |
| packageConfig, | |
| primary: true, | |
| ); | |
| // Add fonts, assets, and licenses from packages in the project's | |
| // dependencies. | |
| // To avoid bundling assets from dev_dependencies and other pub workspace | |
| // packages, we compute the set of transitive dependencies. | |
| final List<Dependency> transitiveDependencies = flutterManifest.isEmpty | |
| ? <Dependency>[] | |
| : computeTransitiveDependencies(flutterProject, packageConfig); | |
| final additionalLicenseFiles = <String, List<File>>{}; | |
| for (final dependency in transitiveDependencies) { | |
| if (!includeAssetsFromDevDependencies && dependency.isExclusiveDevDependency) { | |
| continue; | |
| } | |
| final String packageName = dependency.name; | |
| final Package? package = packageConfig[packageName]; | |
| if (package == null) { | |
| // This can happen with eg. `flutter run --no-pub`. | |
| // | |
| // We usually expect the package config to be up to date with the | |
| // current pubspec.yaml - but because we can force pub get to not be run | |
| // with `flutter run --no-pub` we can end up with a new dependency in | |
| // pubspec.yaml that is not yet discovered by pub and placed in the | |
| // package config. | |
| throwToolExit('Could not locate package:$packageName. Try running `flutter pub get`.'); | |
| } | |
| final Uri packageUri = package.packageUriRoot; | |
| if (packageUri.scheme == 'file') { | |
| final String packageManifestPath = _fileSystem.path.fromUri( | |
| packageUri.resolve('../pubspec.yaml'), | |
| ); | |
| inputFiles.add(_fileSystem.file(packageManifestPath)); | |
| final FlutterManifest? packageFlutterManifest = FlutterManifest.createFromPath( | |
| packageManifestPath, | |
| logger: _logger, | |
| fileSystem: _fileSystem, | |
| ); | |
| if (packageFlutterManifest == null) { | |
| continue; | |
| } | |
| // Collect any additional licenses from each package. | |
| final isAppItself = packageFlutterManifest.appName == flutterManifest.appName; | |
| final licenseFiles = <File>[]; | |
| // Most packages declare no additional licenses, so skip all of the work | |
| // below (including canonicalization) when there is nothing to collect. | |
| if (packageFlutterManifest.additionalLicenses.isNotEmpty) { | |
| // The package root is constant for this package, so canonicalize it | |
| // once instead of per license path. The app itself is exempt from the | |
| // containment check, in which case this stays null. | |
| final String? packageRoot = isAppItself | |
| ? null | |
| : _fileSystem.path.canonicalize(_fileSystem.path.fromUri(package.root)); | |
| for (final String relativeLicensePath in packageFlutterManifest.additionalLicenses) { | |
| final String absoluteLicensePath = _fileSystem.path.fromUri( | |
| package.root.resolve(relativeLicensePath), | |
| ); | |
| // A dependency must not declare a license path that escapes its own | |
| // package directory (e.g. '../secret'). Otherwise the build would read | |
| // an arbitrary file outside the package and bundle its contents into | |
| // the app's NOTICES. This mirrors the containment check applied to | |
| // dependency-declared asset paths in `_ensureAssetPathIsValid`. | |
| if (packageRoot != null) { | |
| final String resolvedLicense = _fileSystem.path.canonicalize(absoluteLicensePath); | |
| if (packageRoot != resolvedLicense && | |
| !_fileSystem.path.isWithin(packageRoot, resolvedLicense)) { | |
| throwToolExit( | |
| 'Package "${packageFlutterManifest.appName}" specified a license path ' | |
| '"$relativeLicensePath" that escapes its package directory. License paths ' | |
| 'declared by a package must stay within that package.', | |
| ); | |
| } | |
| } | |
| licenseFiles.add(_fileSystem.file(absoluteLicensePath).absolute); | |
| } | |
| } | |
| additionalLicenseFiles[packageFlutterManifest.appName] = licenseFiles; | |
| // Skip the app itself | |
| if (packageFlutterManifest.appName == flutterManifest.appName) { | |
| continue; | |
| } | |
| final String packageBasePath = _fileSystem.path.dirname(packageManifestPath); | |
| final Map<_Asset, List<_Asset>>? packageAssets = _parseAssets( | |
| packageConfig, | |
| packageFlutterManifest, | |
| // Do not track wildcard directories for dependencies. | |
| <Uri>[], | |
| packageBasePath, | |
| targetPlatform, | |
| packageName: package.name, | |
| attributedPackage: package, | |
| flavor: flavor, | |
| ); | |
| if (packageAssets == null) { | |
| return 1; | |
| } | |
| assetVariants.addAll(packageAssets); | |
| if (!includesMaterialFonts && packageFlutterManifest.usesMaterialDesign) { | |
| _logger.printError( | |
| 'package:${package.name} has `uses-material-design: true` set but ' | |
| 'the primary pubspec contains `uses-material-design: false`. ' | |
| 'If the application needs material icons, then `uses-material-design` ' | |
| ' must be set to true.', | |
| ); | |
| } | |
| fonts.addAll( | |
| _parseFonts( | |
| packageFlutterManifest, | |
| packageConfig, | |
| packageName: package.name, | |
| primary: false, | |
| ), | |
| ); | |
| } | |
| } | |
| for (final HookAsset dataAsset in flutterHookResult?.dataAssets ?? <HookAsset>[]) { | |
| final Package package = packageConfig[dataAsset.package]!; | |
| final Uri fileUri = dataAsset.file; | |
| final String filePath = fileUri.toFilePath(); | |
| final asset = _Asset( | |
| baseDir: _fileSystem.path.dirname(filePath), | |
| relativeUri: Uri(path: _fileSystem.path.basename(filePath)), | |
| entryUri: Uri.parse(_fileSystem.path.join('packages', dataAsset.package, dataAsset.name)), | |
| package: package, | |
| ); | |
| if (assetVariants.containsKey(asset)) { | |
| _logger.printError( | |
| 'Conflicting assets: The asset "$asset" was declared in the pubspec and the hook.', | |
| ); | |
| return 1; | |
| } | |
| assetVariants[asset] = <_Asset>[asset]; | |
| } | |
| // Save the contents of each image, image variant, and font | |
| // asset in [entries]. | |
| for (final _Asset asset in assetVariants.keys) { | |
| final File assetFile = asset.lookupAssetFile(_fileSystem); | |
| final List<_Asset> variants = assetVariants[asset]!; | |
| if (!assetFile.existsSync() && variants.isEmpty) { | |
| _logger.printStatus('Error detected in pubspec.yaml:', emphasis: true); | |
| _logger.printError('No file or variants found for $asset.\n'); | |
| if (asset.package != null) { | |
| _logger.printError('This asset was included from package ${asset.package?.name}.'); | |
| } | |
| return 1; | |
| } | |
| // The file name for an asset's "main" entry is whatever appears in | |
| // the pubspec.yaml file. The main entry's file must always exist for | |
| // font assets. It need not exist for an image if resolution-specific | |
| // variant files exist. An image's main entry is treated the same as a | |
| // "1x" resolution variant and if both exist then the explicit 1x | |
| // variant is preferred. | |
| if (assetFile.existsSync() && !variants.contains(asset)) { | |
| variants.insert(0, asset); | |
| } | |
| for (final variant in variants) { | |
| final File variantFile = variant.lookupAssetFile(_fileSystem); | |
| inputFiles.add(variantFile); | |
| assert(variantFile.existsSync()); | |
| _setIfConfigurationChanged( | |
| entries, | |
| variant.entryUri.path, | |
| AssetBundleEntry( | |
| DevFSFileContent(variantFile), | |
| kind: variant.kind, | |
| transformers: variant.transformers, | |
| ), | |
| ); | |
| } | |
| } | |
| // Save the contents of each deferred component image, image variant, and font | |
| // asset in deferredComponentsEntries. | |
| for (final String componentName in deferredComponentsAssetVariants.keys) { | |
| deferredComponentsEntries[componentName] = <String, AssetBundleEntry>{}; | |
| final Map<_Asset, List<_Asset>> assetsMap = deferredComponentsAssetVariants[componentName]!; | |
| for (final _Asset asset in assetsMap.keys) { | |
| final File assetFile = asset.lookupAssetFile(_fileSystem); | |
| if (!assetFile.existsSync() && assetsMap[asset]!.isEmpty) { | |
| _logger.printStatus('Error detected in pubspec.yaml:', emphasis: true); | |
| _logger.printError('No file or variants found for $asset.\n'); | |
| if (asset.package != null) { | |
| _logger.printError('This asset was included from package ${asset.package?.name}.'); | |
| } | |
| return 1; | |
| } | |
| // The file name for an asset's "main" entry is whatever appears in | |
| // the pubspec.yaml file. The main entry's file must always exist for | |
| // font assets. It need not exist for an image if resolution-specific | |
| // variant files exist. An image's main entry is treated the same as a | |
| // "1x" resolution variant and if both exist then the explicit 1x | |
| // variant is preferred. | |
| if (assetFile.existsSync() && !assetsMap[asset]!.contains(asset)) { | |
| assetsMap[asset]!.insert(0, asset); | |
| } | |
| for (final _Asset variant in assetsMap[asset]!) { | |
| final File variantFile = variant.lookupAssetFile(_fileSystem); | |
| assert(variantFile.existsSync()); | |
| _setIfConfigurationChanged( | |
| deferredComponentsEntries[componentName]!, | |
| variant.entryUri.path, | |
| AssetBundleEntry( | |
| DevFSFileContent(variantFile), | |
| kind: AssetKind.regular, | |
| transformers: variant.transformers, | |
| ), | |
| ); | |
| } | |
| } | |
| } | |
| final materialAndFrameworkAssets = <_Asset>[ | |
| if (flutterManifest.usesMaterialDesign) ..._getMaterialFonts(), | |
| // For all platforms, include the shaders unconditionally. They are | |
| // small, and whether they're used is determined only by the app source | |
| // code and not by the Flutter manifest. | |
| ..._getFrameworkShaders(), | |
| ]; | |
| for (final asset in materialAndFrameworkAssets) { | |
| final File assetFile = asset.lookupAssetFile(_fileSystem); | |
| assert(assetFile.existsSync(), 'Missing ${assetFile.path}'); | |
| entries[asset.entryUri.path] ??= AssetBundleEntry( | |
| DevFSFileContent(assetFile), | |
| kind: asset.kind, | |
| transformers: const <AssetTransformerEntry>[], | |
| ); | |
| } | |
| // Update wildcard directories we can detect changes in them. | |
| for (final uri in wildcardDirectories) { | |
| _wildcardDirectories[uri] ??= _fileSystem.directory(uri); | |
| } | |
| final Map<String, List<String>> assetManifest = _createAssetManifest( | |
| assetVariants, | |
| deferredComponentsAssetVariants, | |
| ); | |
| final DevFSByteContent assetManifestBinary = _createAssetManifestBinary(assetManifest); | |
| final fontManifest = DevFSStringContent(json.encode(fonts)); | |
| final LicenseResult licenseResult = _licenseCollector.obtainLicenses( | |
| packageConfig, | |
| additionalLicenseFiles, | |
| ); | |
| if (licenseResult.errorMessages.isNotEmpty) { | |
| licenseResult.errorMessages.forEach(_logger.printError); | |
| return 1; | |
| } | |
| additionalDependencies = licenseResult.dependencies; | |
| inputFiles.addAll(additionalDependencies); | |
| if (wildcardDirectories.isNotEmpty) { | |
| // Force the depfile to contain missing files so that Gradle does not skip | |
| // the task. Wildcard directories are not compatible with full incremental | |
| // builds. For more context see https://github.com/flutter/flutter/issues/56466 . | |
| _logger.printTrace( | |
| 'Manifest contained wildcard assets. Inserting missing file into ' | |
| 'build graph to force rerun. for more information see #56466.', | |
| ); | |
| final suffix = Object().hashCode; | |
| additionalDependencies.add( | |
| _fileSystem.file('DOES_NOT_EXIST_RERUN_FOR_WILDCARD$suffix').absolute, | |
| ); | |
| } | |
| _setIfChanged(_kAssetManifestBinFilename, assetManifestBinary, AssetKind.regular); | |
| // Create .bin.json on web builds. | |
| if (targetPlatform == TargetPlatform.web_javascript) { | |
| final assetManifestBinaryJson = DevFSStringContent( | |
| json.encode(base64.encode(assetManifestBinary.bytes)), | |
| ); | |
| _setIfChanged(_kAssetManifestBinJsonFilename, assetManifestBinaryJson, AssetKind.regular); | |
| } | |
| _setIfChanged(kFontManifestJson, fontManifest, AssetKind.regular); | |
| _setLicenseIfChanged(licenseResult.combinedLicenses, targetPlatform); | |
| return 0; | |
| } | |
| @override | |
| List<File> additionalDependencies = <File>[]; | |
| void _setIfChanged(String key, DevFSContent content, AssetKind assetKind) { | |
| final DevFSContent? oldContent = entries[key]?.content; | |
| // In the case that the content is unchanged, we want to avoid an overwrite | |
| // as the isModified property may be reset to true, | |
| if (oldContent is DevFSByteContent && | |
| content is DevFSByteContent && | |
| _compareIntLists(oldContent.bytes, content.bytes)) { | |
| return; | |
| } | |
| entries[key] = AssetBundleEntry( | |
| content, | |
| kind: assetKind, | |
| transformers: const <AssetTransformerEntry>[], | |
| ); | |
| } | |
| static bool _compareIntLists(List<int> o1, List<int> o2) { | |
| if (o1.length != o2.length) { | |
| return false; | |
| } | |
| for (var index = 0; index < o1.length; index++) { | |
| if (o1[index] != o2[index]) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| void _setIfConfigurationChanged( | |
| Map<String, AssetBundleEntry> entryMap, | |
| String key, | |
| AssetBundleEntry entry, | |
| ) { | |
| final AssetBundleEntry? existingEntry = entryMap[key]; | |
| if (existingEntry == null || !entry.hasEquivalentConfigurationWith(existingEntry)) { | |
| entryMap[key] = entry; | |
| } | |
| } | |
| void _setLicenseIfChanged(String combinedLicenses, TargetPlatform targetPlatform) { | |
| // On the web, don't compress the NOTICES file since the client doesn't have | |
| // dart:io to decompress it. So use the standard _setIfChanged to check if | |
| // the strings still match. | |
| if (targetPlatform == TargetPlatform.web_javascript) { | |
| _setIfChanged(_kNoticeFile, DevFSStringContent(combinedLicenses), AssetKind.regular); | |
| return; | |
| } | |
| // On other platforms, let the NOTICES file be compressed. But use a | |
| // specialized DevFSStringCompressingBytesContent class to compare | |
| // the uncompressed strings to not incur decompression/decoding while making | |
| // the comparison. | |
| if (!entries.containsKey(_kNoticeZippedFile) || | |
| (entries[_kNoticeZippedFile]?.content as DevFSStringCompressingBytesContent?)?.equals( | |
| combinedLicenses, | |
| ) != | |
| true) { | |
| entries[_kNoticeZippedFile] = AssetBundleEntry( | |
| DevFSStringCompressingBytesContent( | |
| combinedLicenses, | |
| // A zlib dictionary is a hinting string sequence with the most | |
| // likely string occurrences at the end. This ends up just being | |
| // common English words with domain specific words like copyright. | |
| hintString: 'copyrightsoftwaretothisinandorofthe', | |
| ), | |
| kind: AssetKind.regular, | |
| transformers: const <AssetTransformerEntry>[], | |
| ); | |
| } | |
| } | |
| List<_Asset> _getMaterialFonts() { | |
| final result = <_Asset>[]; | |
| for (final Map<String, Object> family in kMaterialFonts) { | |
| final Object? fonts = family['fonts']; | |
| if (fonts == null) { | |
| continue; | |
| } | |
| for (final Map<String, Object> font in fonts as List<Map<String, String>>) { | |
| final asset = font['asset'] as String?; | |
| if (asset == null) { | |
| continue; | |
| } | |
| final Uri entryUri = _fileSystem.path.toUri(asset); | |
| result.add( | |
| _Asset( | |
| baseDir: _fileSystem.path.join( | |
| _flutterRoot, | |
| 'bin', | |
| 'cache', | |
| 'artifacts', | |
| 'material_fonts', | |
| ), | |
| relativeUri: Uri(path: entryUri.pathSegments.last), | |
| entryUri: entryUri, | |
| package: null, | |
| kind: AssetKind.font, | |
| ), | |
| ); | |
| } | |
| } | |
| return result; | |
| } | |
| List<_Asset> _getFrameworkShaders() { | |
| final result = <_Asset>[]; | |
| final String materialShaderPath = _fileSystem.path.join( | |
| _flutterRoot, | |
| 'packages', | |
| 'flutter', | |
| 'lib', | |
| 'src', | |
| 'material', | |
| 'shaders', | |
| ); | |
| if (_fileSystem.directory(materialShaderPath).existsSync()) { | |
| // TODO(chunhtai): remove ink_sparkle.frag sideloading. | |
| // https://github.com/flutter/flutter/issues/188545. | |
| result.add( | |
| _Asset( | |
| baseDir: materialShaderPath, | |
| relativeUri: Uri(path: 'ink_sparkle.frag'), | |
| entryUri: _fileSystem.path.toUri('shaders/ink_sparkle.frag'), | |
| package: null, | |
| kind: AssetKind.shader, | |
| ), | |
| ); | |
| } | |
| final String widgetsShaderPath = _fileSystem.path.join( | |
| _flutterRoot, | |
| 'packages', | |
| 'flutter', | |
| 'lib', | |
| 'src', | |
| 'widgets', | |
| 'shaders', | |
| ); | |
| if (_fileSystem.directory(widgetsShaderPath).existsSync()) { | |
| result.add( | |
| _Asset( | |
| baseDir: widgetsShaderPath, | |
| relativeUri: Uri(path: 'stretch_effect.frag'), | |
| entryUri: _fileSystem.path.toUri('shaders/stretch_effect.frag'), | |
| package: null, | |
| kind: AssetKind.shader, | |
| ), | |
| ); | |
| } | |
| return result; | |
| } | |
| List<Map<String, Object?>> _parseFonts( | |
| FlutterManifest manifest, | |
| PackageConfig packageConfig, { | |
| String? packageName, | |
| required bool primary, | |
| }) { | |
| return <Map<String, Object?>>[ | |
| if (primary && manifest.usesMaterialDesign) ...kMaterialFonts, | |
| if (packageName == null) | |
| ...manifest.fontsDescriptor | |
| else | |
| for (final Font font in _parsePackageFonts(manifest, packageName, packageConfig)) | |
| font.descriptor, | |
| ]; | |
| } | |
| Map<String, Map<_Asset, List<_Asset>>> _parseDeferredComponentsAssets( | |
| FlutterManifest flutterManifest, | |
| PackageConfig packageConfig, | |
| String assetBasePath, | |
| List<Uri> wildcardDirectories, | |
| Directory projectDirectory, { | |
| required TargetPlatform targetPlatform, | |
| String? flavor, | |
| }) { | |
| final List<DeferredComponent>? components = flutterManifest.deferredComponents; | |
| final deferredComponentsAssetVariants = <String, Map<_Asset, List<_Asset>>>{}; | |
| if (components == null) { | |
| return deferredComponentsAssetVariants; | |
| } | |
| for (final DeferredComponent component in components) { | |
| final cache = _AssetDirectoryCache(_fileSystem); | |
| final componentAssets = <_Asset, List<_Asset>>{}; | |
| for (final AssetsEntry assetsEntry in component.assets) { | |
| if (assetsEntry.uri.path.endsWith('/')) { | |
| wildcardDirectories.add(assetsEntry.uri); | |
| _parseAssetsFromFolder( | |
| packageConfig, | |
| flutterManifest, | |
| assetBasePath, | |
| cache, | |
| componentAssets, | |
| assetsEntry.uri, | |
| flavors: assetsEntry.flavors, | |
| platforms: assetsEntry.platforms, | |
| transformers: assetsEntry.transformers, | |
| ); | |
| } else { | |
| _parseAssetFromFile( | |
| packageConfig, | |
| flutterManifest, | |
| assetBasePath, | |
| cache, | |
| componentAssets, | |
| assetsEntry.uri, | |
| flavors: assetsEntry.flavors, | |
| platforms: assetsEntry.platforms, | |
| transformers: assetsEntry.transformers, | |
| ); | |
| } | |
| } | |
| componentAssets.removeWhere( | |
| (_Asset asset, List<_Asset> variants) => | |
| !asset.matchesFlavor(flavor) || !asset.matchesPlatform(targetPlatform), | |
| ); | |
| deferredComponentsAssetVariants[component.name] = componentAssets; | |
| } | |
| return deferredComponentsAssetVariants; | |
| } | |
| Map<String, List<String>> _createAssetManifest( | |
| Map<_Asset, List<_Asset>> assetVariants, | |
| Map<String, Map<_Asset, List<_Asset>>> deferredComponentsAssetVariants, | |
| ) { | |
| final manifest = <String, List<String>>{}; | |
| final entries = <_Asset, List<String>>{}; | |
| assetVariants.forEach((_Asset main, List<_Asset> variants) { | |
| entries[main] = <String>[for (final _Asset variant in variants) variant.entryUri.path]; | |
| }); | |
| for (final Map<_Asset, List<_Asset>> componentAssets | |
| in deferredComponentsAssetVariants.values) { | |
| componentAssets.forEach((_Asset main, List<_Asset> variants) { | |
| entries[main] = <String>[for (final _Asset variant in variants) variant.entryUri.path]; | |
| }); | |
| } | |
| final List<_Asset> sortedKeys = entries.keys.toList() | |
| ..sort((_Asset left, _Asset right) => left.entryUri.path.compareTo(right.entryUri.path)); | |
| for (final main in sortedKeys) { | |
| final String decodedEntryPath = Uri.decodeFull(main.entryUri.path); | |
| final List<String> rawEntryVariantsPaths = entries[main]!; | |
| final List<String> decodedEntryVariantPaths = rawEntryVariantsPaths | |
| .map((String value) => Uri.decodeFull(value)) | |
| .toList(); | |
| manifest[decodedEntryPath] = decodedEntryVariantPaths; | |
| } | |
| return manifest; | |
| } | |
| // Matches path-like strings ending in a number followed by an 'x'. | |
| // Example matches include "assets/animals/2.0x", "plants/3x", and "2.7x". | |
| static final _extractPixelRatioFromKeyRegExp = RegExp(r'/?(\d+(\.\d*)?)x$'); | |
| DevFSByteContent _createAssetManifestBinary(Map<String, List<String>> assetManifest) { | |
| double? parseScale(String key) { | |
| final Uri assetUri = Uri.parse(key); | |
| var directoryPath = ''; | |
| if (assetUri.pathSegments.length > 1) { | |
| directoryPath = assetUri.pathSegments[assetUri.pathSegments.length - 2]; | |
| } | |
| final Match? match = _extractPixelRatioFromKeyRegExp.firstMatch(directoryPath); | |
| if (match != null && match.groupCount > 0) { | |
| return double.parse(match.group(1)!); | |
| } | |
| return null; | |
| } | |
| final result = <String, dynamic>{}; | |
| for (final MapEntry<String, dynamic> manifestEntry in assetManifest.entries) { | |
| final resultVariants = <dynamic>[]; | |
| final List<String> entries = (manifestEntry.value as List<dynamic>).cast<String>(); | |
| for (final variant in entries) { | |
| final resultVariant = <String, dynamic>{}; | |
| final double? variantDevicePixelRatio = parseScale(variant); | |
| resultVariant['asset'] = variant; | |
| if (variantDevicePixelRatio != null) { | |
| resultVariant['dpr'] = variantDevicePixelRatio; | |
| } | |
| resultVariants.add(resultVariant); | |
| } | |
| result[manifestEntry.key] = resultVariants; | |
| } | |
| final ByteData message = const StandardMessageCodec().encodeMessage(result)!; | |
| return DevFSByteContent(message.buffer.asUint8List(0, message.lengthInBytes)); | |
| } | |
| /// Prefixes family names and asset paths of fonts included from packages with | |
| /// `packages/<package_name>`. | |
| List<Font> _parsePackageFonts( | |
| FlutterManifest manifest, | |
| String packageName, | |
| PackageConfig packageConfig, | |
| ) { | |
| final packageFonts = <Font>[]; | |
| for (final Font font in manifest.fonts) { | |
| final packageFontAssets = <FontAsset>[]; | |
| for (final FontAsset fontAsset in font.fontAssets) { | |
| final Uri assetUri = fontAsset.assetUri; | |
| if (assetUri.pathSegments.first == 'packages' && | |
| !_fileSystem.isFileSync( | |
| _fileSystem.path.fromUri( | |
| packageConfig[packageName]?.packageUriRoot.resolve('../${assetUri.path}'), | |
| ), | |
| )) { | |
| packageFontAssets.add( | |
| FontAsset(fontAsset.assetUri, weight: fontAsset.weight, style: fontAsset.style), | |
| ); | |
| } else { | |
| packageFontAssets.add( | |
| FontAsset( | |
| Uri(pathSegments: <String>['packages', packageName, ...assetUri.pathSegments]), | |
| weight: fontAsset.weight, | |
| style: fontAsset.style, | |
| ), | |
| ); | |
| } | |
| } | |
| packageFonts.add(Font('packages/$packageName/${font.familyName}', packageFontAssets)); | |
| } | |
| return packageFonts; | |
| } | |
| /// Given an assetBase location and a pubspec.yaml Flutter manifest, return a | |
| /// map of assets to asset variants. | |
| /// | |
| /// Returns null on missing assets. | |
| /// | |
| /// Given package: 'test_package' and an assets directory like this: | |
| /// | |
| /// - assets/foo | |
| /// - assets/var1/foo | |
| /// - assets/var2/foo | |
| /// - assets/bar | |
| /// | |
| /// This will return: | |
| /// ```none | |
| /// { | |
| /// asset: packages/test_package/assets/foo: [ | |
| /// asset: packages/test_package/assets/foo, | |
| /// asset: packages/test_package/assets/var1/foo, | |
| /// asset: packages/test_package/assets/var2/foo, | |
| /// ], | |
| /// asset: packages/test_package/assets/bar: [ | |
| /// asset: packages/test_package/assets/bar, | |
| /// ], | |
| /// } | |
| /// ``` | |
| Map<_Asset, List<_Asset>>? _parseAssets( | |
| PackageConfig packageConfig, | |
| FlutterManifest flutterManifest, | |
| List<Uri> wildcardDirectories, | |
| String assetBase, | |
| TargetPlatform targetPlatform, { | |
| String? packageName, | |
| Package? attributedPackage, | |
| required String? flavor, | |
| }) { | |
| final result = <_Asset, List<_Asset>>{}; | |
| final cache = _AssetDirectoryCache(_fileSystem); | |
| for (final AssetsEntry assetsEntry in flutterManifest.assets) { | |
| if (assetsEntry.uri.path.endsWith('/')) { | |
| wildcardDirectories.add(assetsEntry.uri); | |
| _parseAssetsFromFolder( | |
| packageConfig, | |
| flutterManifest, | |
| assetBase, | |
| cache, | |
| result, | |
| assetsEntry.uri, | |
| packageName: packageName, | |
| attributedPackage: attributedPackage, | |
| flavors: assetsEntry.flavors, | |
| platforms: assetsEntry.platforms, | |
| transformers: assetsEntry.transformers, | |
| ); | |
| } else { | |
| _parseAssetFromFile( | |
| packageConfig, | |
| flutterManifest, | |
| assetBase, | |
| cache, | |
| result, | |
| assetsEntry.uri, | |
| packageName: packageName, | |
| attributedPackage: attributedPackage, | |
| flavors: assetsEntry.flavors, | |
| platforms: assetsEntry.platforms, | |
| transformers: assetsEntry.transformers, | |
| ); | |
| } | |
| } | |
| for (final AssetsEntry shaderEntry in flutterManifest.shaders) { | |
| final Uri shaderUri = shaderEntry.uri; | |
| for (final AssetsEntry assetEntry in flutterManifest.assets) { | |
| final String assetPath = assetEntry.uri.path; | |
| final String shaderPath = shaderUri.path; | |
| if (assetPath == shaderPath) { | |
| _logger.printError( | |
| 'Error: Shader "$shaderPath" is also defined as an asset. Shaders ' | |
| 'should only be defined in the "shaders" section of the ' | |
| 'pubspec.yaml, not in the "assets" section.', | |
| ); | |
| return null; | |
| } | |
| if (assetPath.endsWith('/') && shaderPath.startsWith(assetPath)) { | |
| _logger.printError( | |
| 'Error: Shader "$shaderPath" is included in the asset directory ' | |
| '"$assetPath". Shaders should only be defined in the "shaders" ' | |
| 'section of the pubspec.yaml, not in the "assets" section.', | |
| ); | |
| return null; | |
| } | |
| } | |
| _parseAssetFromFile( | |
| packageConfig, | |
| flutterManifest, | |
| assetBase, | |
| cache, | |
| result, | |
| shaderUri, | |
| packageName: packageName, | |
| attributedPackage: attributedPackage, | |
| assetKind: AssetKind.shader, | |
| flavors: shaderEntry.flavors, | |
| platforms: shaderEntry.platforms, | |
| transformers: shaderEntry.transformers, | |
| ); | |
| } | |
| result.removeWhere((_Asset asset, List<_Asset> variants) { | |
| if (!asset.matchesFlavor(flavor)) { | |
| _logger.printTrace( | |
| 'Skipping assets entry "${asset.entryUri.path}" since ' | |
| 'its configured flavor(s) did not match the provided flavor (if any).\n' | |
| 'Configured flavors: ${asset.flavors.join(', ')}\n', | |
| ); | |
| return true; | |
| } | |
| if (!asset.matchesPlatform(targetPlatform)) { | |
| _logger.printTrace( | |
| 'Skipping assets entry "${asset.entryUri.path}" since ' | |
| 'its configured platform(s) did not match the target platform.\n' | |
| 'Configured platforms: ${asset.platforms.join(', ')}\n' | |
| 'Target platform: ${targetPlatform.osName}\n', | |
| ); | |
| return true; | |
| } | |
| return false; | |
| }); | |
| // Add assets referenced in the fonts section of the manifest. | |
| for (final Font font in flutterManifest.fonts) { | |
| for (final FontAsset fontAsset in font.fontAssets) { | |
| final _Asset baseAsset = _resolveAsset( | |
| packageConfig, | |
| assetBase, | |
| fontAsset.assetUri, | |
| packageName, | |
| attributedPackage, | |
| assetKind: AssetKind.font, | |
| flavors: <String>{}, | |
| platforms: <String>{}, | |
| transformers: <AssetTransformerEntry>[], | |
| ); | |
| final File baseAssetFile = baseAsset.lookupAssetFile(_fileSystem); | |
| if (!baseAssetFile.existsSync()) { | |
| _logger.printError( | |
| 'Error: unable to locate asset entry in pubspec.yaml: "${fontAsset.assetUri}".', | |
| ); | |
| return null; | |
| } | |
| result[baseAsset] = <_Asset>[]; | |
| } | |
| } | |
| return result; | |
| } | |
| void _parseAssetsFromFolder( | |
| PackageConfig packageConfig, | |
| FlutterManifest flutterManifest, | |
| String assetBase, | |
| _AssetDirectoryCache cache, | |
| Map<_Asset, List<_Asset>> result, | |
| Uri assetUri, { | |
| String? packageName, | |
| Package? attributedPackage, | |
| required Set<String> flavors, | |
| required Set<String> platforms, | |
| required List<AssetTransformerEntry> transformers, | |
| }) { | |
| final String directoryPath; | |
| _ensureAssetPathIsValid(assetsBaseDir: assetBase, assetUri: assetUri, packageName: packageName); | |
| final String localDirectoryPath = _fileSystem.path.join( | |
| assetBase, | |
| assetUri.toFilePath(windows: _platform.isWindows), | |
| ); | |
| final bool isPackageAsset = | |
| assetUri.pathSegments.length >= 2 && | |
| assetUri.pathSegments[1].isNotEmpty && | |
| assetUri.pathSegments.first == 'packages' && | |
| !_fileSystem.directory(localDirectoryPath).existsSync(); | |
| String? packageLibPath; | |
| String? resolvedPackageName; | |
| if (isPackageAsset) { | |
| final _Asset? packageAsset = _resolvePackageAsset( | |
| assetUri, | |
| packageConfig, | |
| attributedPackage, | |
| flavors: flavors, | |
| platforms: platforms, | |
| transformers: transformers, | |
| ); | |
| if (packageAsset == null) { | |
| return; | |
| } | |
| directoryPath = _fileSystem.path.join( | |
| packageAsset.baseDir, | |
| packageAsset.relativeUri.toFilePath(windows: _platform.isWindows), | |
| ); | |
| packageLibPath = packageAsset.baseDir; | |
| resolvedPackageName = assetUri.pathSegments[1]; | |
| } else { | |
| directoryPath = localDirectoryPath; | |
| } | |
| if (!_fileSystem.directory(directoryPath).existsSync()) { | |
| _logger.printError('Error: unable to find directory entry in pubspec.yaml: $directoryPath'); | |
| return; | |
| } | |
| final Iterable<FileSystemEntity> entities = _fileSystem.directory(directoryPath).listSync(); | |
| final Iterable<File> files = entities.whereType<File>(); | |
| for (final file in files) { | |
| final Uri uri; | |
| if (isPackageAsset) { | |
| final String relativePathToLib = _fileSystem.path.relative(file.path, from: packageLibPath); | |
| final List<String> parts = _fileSystem.path.split(relativePathToLib); | |
| uri = Uri(pathSegments: <String>['packages', resolvedPackageName!, ...parts]); | |
| } else { | |
| final String relativePath = _fileSystem.path.relative(file.path, from: assetBase); | |
| uri = Uri.file(relativePath, windows: _platform.isWindows); | |
| } | |
| _parseAssetFromFile( | |
| packageConfig, | |
| flutterManifest, | |
| assetBase, | |
| cache, | |
| result, | |
| uri, | |
| packageName: packageName, | |
| attributedPackage: attributedPackage, | |
| originUri: assetUri, | |
| flavors: flavors, | |
| platforms: platforms, | |
| transformers: transformers, | |
| ); | |
| } | |
| } | |
| void _parseAssetFromFile( | |
| PackageConfig packageConfig, | |
| FlutterManifest flutterManifest, | |
| String assetBase, | |
| _AssetDirectoryCache cache, | |
| Map<_Asset, List<_Asset>> result, | |
| Uri assetUri, { | |
| Uri? originUri, | |
| String? packageName, | |
| Package? attributedPackage, | |
| AssetKind assetKind = AssetKind.regular, | |
| required Set<String> flavors, | |
| required Set<String> platforms, | |
| required List<AssetTransformerEntry> transformers, | |
| }) { | |
| final _Asset asset = _resolveAsset( | |
| packageConfig, | |
| assetBase, | |
| assetUri, | |
| packageName, | |
| attributedPackage, | |
| assetKind: assetKind, | |
| originUri: originUri, | |
| flavors: flavors, | |
| platforms: platforms, | |
| transformers: transformers, | |
| ); | |
| _checkForFlavorConflicts(asset, result.keys.toList()); | |
| final variants = <_Asset>[]; | |
| final File assetFile = asset.lookupAssetFile(_fileSystem); | |
| for (final String path in cache.variantsFor(assetFile.path)) { | |
| final String relativePath = _fileSystem.path.relative(path, from: asset.baseDir); | |
| final Uri relativeUri = _fileSystem.path.toUri(relativePath); | |
| final Uri? entryUri = asset.symbolicPrefixUri == null | |
| ? relativeUri | |
| : asset.symbolicPrefixUri?.resolveUri(relativeUri); | |
| if (entryUri != null) { | |
| variants.add( | |
| _Asset( | |
| baseDir: asset.baseDir, | |
| entryUri: entryUri, | |
| relativeUri: relativeUri, | |
| package: attributedPackage, | |
| kind: assetKind, | |
| flavors: flavors, | |
| platforms: platforms, | |
| transformers: transformers, | |
| ), | |
| ); | |
| } | |
| } | |
| result[asset] = variants; | |
| } | |
| // Since it is not clear how overlapping asset declarations should work in the | |
| // presence of conditions such as `flavor`, we throw an Error. | |
| // | |
| // To be more specific, it is not clear if conditions should be combined with | |
| // or-logic or and-logic, or if it should depend on the specificity of the | |
| // declarations (file versus directory). If you would like examples, consider these: | |
| // | |
| // ```yaml | |
| // # Should assets/free.mp3 always be included since "assets/" has no flavor? | |
| // assets: | |
| // - assets/ | |
| // - path: assets/free.mp3 | |
| // flavor: free | |
| // | |
| // # Should "assets/paid/pip.mp3" be included for both the "paid" and "free" flavors? | |
| // # Or, since "assets/paid/pip.mp3" is more specific than "assets/paid/"", should | |
| // # it take precedence over the latter (included only in "free" flavor)? | |
| // assets: | |
| // - path: assets/paid/ | |
| // flavor: paid | |
| // - path: assets/paid/pip.mp3 | |
| // flavor: free | |
| // - asset | |
| // ``` | |
| // | |
| // Since it is not obvious what logic (if any) would be intuitive and preferable | |
| // to the vast majority of users (if any), we play it safe by throwing a `ToolExit` | |
| // in any of these situations. We can always loosen up this restriction later | |
| // without breaking anyone. | |
| void _checkForFlavorConflicts(_Asset newAsset, List<_Asset> previouslyParsedAssets) { | |
| bool cameFromDirectoryEntry(_Asset asset) { | |
| return asset.originUri.path.endsWith('/'); | |
| } | |
| String flavorErrorInfo(_Asset asset) { | |
| if (asset.flavors.isEmpty) { | |
| return 'An entry with the path "${asset.originUri}" does not specify any flavors.'; | |
| } | |
| final Iterable<String> flavorsWrappedWithQuotes = asset.flavors.map((String e) => '"$e"'); | |
| return 'An entry with the path "${asset.originUri}" specifies the flavor(s): ' | |
| '${flavorsWrappedWithQuotes.join(', ')}.'; | |
| } | |
| final _Asset? preExistingAsset = previouslyParsedAssets | |
| .where((_Asset other) => other.entryUri == newAsset.entryUri) | |
| .firstOrNull; | |
| if (preExistingAsset == null || preExistingAsset.hasEquivalentFlavorsWith(newAsset)) { | |
| return; | |
| } | |
| final errorMessage = StringBuffer( | |
| 'Multiple assets entries include the file ' | |
| '"${newAsset.entryUri.path}", but they specify different lists of flavors.\n', | |
| ); | |
| errorMessage.writeln(flavorErrorInfo(preExistingAsset)); | |
| errorMessage.writeln(flavorErrorInfo(newAsset)); | |
| if (cameFromDirectoryEntry(newAsset) || cameFromDirectoryEntry(preExistingAsset)) { | |
| errorMessage.writeln(); | |
| errorMessage.write( | |
| 'Consider organizing assets with different flavors ' | |
| 'into different directories.', | |
| ); | |
| } | |
| throwToolExit(errorMessage.toString()); | |
| } | |
| void _ensureAssetPathIsValid({ | |
| required String assetsBaseDir, | |
| required Uri assetUri, | |
| String? packageName, | |
| }) { | |
| if (!assetUri.isScheme('file') && assetUri.scheme.isNotEmpty) { | |
| throwToolExit( | |
| 'Asset path "$assetUri" has scheme "${assetUri.scheme}" and is not a valid file or ' | |
| 'directory path. Please update this entry in the pubspec.yaml to point to a valid file ' | |
| 'path.', | |
| ); | |
| } | |
| if (Uri.directory( | |
| assetsBaseDir, | |
| windows: _platform.isWindows, | |
| ).resolveUri(assetUri).toFilePath(windows: _platform.isWindows) == | |
| assetUri.toFilePath(windows: _platform.isWindows)) { | |
| throwToolExit( | |
| '"${assetUri.toFilePath(windows: _platform.isWindows)}" is not a valid asset path. ' | |
| 'Asset paths must be relative to the location of pubspec.yaml. Please update this entry ' | |
| 'in the pubspec.yaml to use a relative path.', | |
| ); | |
| } | |
| // An asset declared by a dependency must stay within that package's own directory. A relative | |
| // path that escapes the package (e.g. '../secret') would otherwise make the build read and | |
| // bundle files from outside the package on the build machine, which the consuming app never | |
| // declared. (The absolute-path check above is preserved for all assets.) | |
| if (packageName != null) { | |
| final String assetPath = assetUri.toFilePath(windows: _platform.isWindows); | |
| final String base = _fileSystem.path.canonicalize(assetsBaseDir); | |
| final String resolved = _fileSystem.path.canonicalize( | |
| _fileSystem.path.join(assetsBaseDir, assetPath), | |
| ); | |
| if (base != resolved && !_fileSystem.path.isWithin(base, resolved)) { | |
| throwToolExit( | |
| 'Asset path "$assetPath" of package ' | |
| '"$packageName" resolves to a location outside the package directory. Package asset ' | |
| 'paths must stay within the package.', | |
| ); | |
| } | |
| } | |
| } | |
| _Asset _resolveAsset( | |
| PackageConfig packageConfig, | |
| String assetsBaseDir, | |
| Uri assetUri, | |
| String? packageName, | |
| Package? attributedPackage, { | |
| Uri? originUri, | |
| AssetKind assetKind = AssetKind.regular, | |
| required Set<String> flavors, | |
| required Set<String> platforms, | |
| required List<AssetTransformerEntry> transformers, | |
| }) { | |
| _ensureAssetPathIsValid( | |
| assetsBaseDir: assetsBaseDir, | |
| assetUri: assetUri, | |
| packageName: packageName, | |
| ); | |
| if (assetUri.pathSegments.first == 'packages' && | |
| !_fileSystem.isFileSync( | |
| _fileSystem.path.join(assetsBaseDir, _fileSystem.path.fromUri(assetUri)), | |
| )) { | |
| // The asset is referenced in the pubspec.yaml as | |
| // 'packages/PACKAGE_NAME/PATH/TO/ASSET . | |
| final _Asset? packageAsset = _resolvePackageAsset( | |
| assetUri, | |
| packageConfig, | |
| attributedPackage, | |
| assetKind: assetKind, | |
| originUri: originUri, | |
| flavors: flavors, | |
| platforms: platforms, | |
| transformers: transformers, | |
| ); | |
| if (packageAsset != null) { | |
| return packageAsset; | |
| } | |
| } | |
| return _Asset( | |
| baseDir: assetsBaseDir, | |
| entryUri: packageName == null | |
| ? assetUri // Asset from the current application. | |
| : Uri( | |
| pathSegments: <String>['packages', packageName, ...assetUri.pathSegments], | |
| ), // Asset from, and declared in $packageName. | |
| relativeUri: assetUri, | |
| package: attributedPackage, | |
| originUri: originUri, | |
| kind: assetKind, | |
| flavors: flavors, | |
| platforms: platforms, | |
| transformers: transformers, | |
| ); | |
| } | |
| _Asset? _resolvePackageAsset( | |
| Uri assetUri, | |
| PackageConfig packageConfig, | |
| Package? attributedPackage, { | |
| AssetKind assetKind = AssetKind.regular, | |
| Uri? originUri, | |
| Set<String>? flavors, | |
| Set<String>? platforms, | |
| List<AssetTransformerEntry>? transformers, | |
| }) { | |
| assert(assetUri.pathSegments.first == 'packages'); | |
| if (assetUri.pathSegments.length > 1) { | |
| final String packageName = assetUri.pathSegments[1]; | |
| final Package? package = packageConfig[packageName]; | |
| final Uri? packageUri = package?.packageUriRoot; | |
| if (packageUri != null && packageUri.scheme == 'file') { | |
| return _Asset( | |
| baseDir: _fileSystem.path.fromUri(packageUri), | |
| entryUri: assetUri, | |
| relativeUri: Uri(pathSegments: assetUri.pathSegments.sublist(2)), | |
| package: attributedPackage, | |
| kind: assetKind, | |
| originUri: originUri, | |
| flavors: flavors, | |
| platforms: platforms, | |
| transformers: transformers, | |
| ); | |
| } | |
| } | |
| _logger.printStatus('Error detected in pubspec.yaml:', emphasis: true); | |
| _logger.printError('Could not resolve package for asset $assetUri.\n'); | |
| if (attributedPackage != null) { | |
| _logger.printError('This asset was included from package ${attributedPackage.name}'); | |
| } | |
| return null; | |
| } | |
| } | |
| @immutable | |
| class _Asset { | |
| const _Asset({ | |
| required this.baseDir, | |
| Uri? originUri, | |
| required this.relativeUri, | |
| required this.entryUri, | |
| required this.package, | |
| this.kind = AssetKind.regular, | |
| Set<String>? flavors, | |
| Set<String>? platforms, | |
| List<AssetTransformerEntry>? transformers, | |
| }) : originUri = originUri ?? entryUri, | |
| flavors = flavors ?? const <String>{}, | |
| platforms = platforms ?? const <String>{}, | |
| transformers = transformers ?? const <AssetTransformerEntry>[]; | |
| final String baseDir; | |
| final Package? package; | |
| /// The platform-independent URL provided by the user in the pubspec that this | |
| /// asset was found from. | |
| final Uri originUri; | |
| /// A platform-independent URL where this asset can be found on disk on the | |
| /// host system relative to [baseDir]. | |
| final Uri relativeUri; | |
| /// A platform-independent URL representing the entry for the asset manifest. | |
| final Uri entryUri; | |
| final AssetKind kind; | |
| final Set<String> flavors; | |
| final Set<String> platforms; | |
| final List<AssetTransformerEntry> transformers; | |
| File lookupAssetFile(FileSystem fileSystem) { | |
| return fileSystem.file(fileSystem.path.join(baseDir, fileSystem.path.fromUri(relativeUri))); | |
| } | |
| /// The delta between what the entryUri is and the relativeUri (e.g., | |
| /// packages/flutter_gallery). | |
| Uri? get symbolicPrefixUri { | |
| if (entryUri == relativeUri) { | |
| return null; | |
| } | |
| final int index = entryUri.path.indexOf(relativeUri.path); | |
| return index == -1 ? null : Uri(path: entryUri.path.substring(0, index)); | |
| } | |
| bool matchesFlavor(String? flavor) { | |
| if (flavors.isEmpty) { | |
| return true; | |
| } | |
| if (flavor == null) { | |
| return false; | |
| } | |
| return flavors.contains(flavor); | |
| } | |
| bool matchesPlatform(TargetPlatform targetPlatform) { | |
| if (platforms.isEmpty || targetPlatform == TargetPlatform.tester) { | |
| return true; | |
| } | |
| return platforms.contains(targetPlatform.osName); | |
| } | |
| bool hasEquivalentFlavorsWith(_Asset other) { | |
| return setEquals(flavors, other.flavors); | |
| } | |
| bool hasEquivalentPlatformsWith(_Asset other) { | |
| return setEquals(platforms, other.platforms); | |
| } | |
| @override | |
| String toString() => 'asset: $entryUri'; | |
| @override | |
| bool operator ==(Object other) { | |
| if (identical(other, this)) { | |
| return true; | |
| } | |
| if (other.runtimeType != runtimeType) { | |
| return false; | |
| } | |
| return other is _Asset && | |
| other.baseDir == baseDir && | |
| other.relativeUri == relativeUri && | |
| other.entryUri == entryUri && | |
| other.kind == kind && | |
| hasEquivalentFlavorsWith(other) && | |
| hasEquivalentPlatformsWith(other); | |
| } | |
| @override | |
| int get hashCode => | |
| Object.hashAll(<Object>[baseDir, relativeUri, entryUri, kind, ...flavors, ...platforms]); | |
| } | |
| // Given an assets directory like this: | |
| // | |
| // assets/foo.png | |
| // assets/2x/foo.png | |
| // assets/3.0x/foo.png | |
| // assets/bar/foo.png | |
| // assets/bar.png | |
| // | |
| // variantsFor('assets/foo.png') => ['/assets/foo.png', '/assets/2x/foo.png', 'assets/3.0x/foo.png'] | |
| // variantsFor('assets/bar.png') => ['/assets/bar.png'] | |
| // variantsFor('assets/bar/foo.png') => ['/assets/bar/foo.png'] | |
| class _AssetDirectoryCache { | |
| _AssetDirectoryCache(this._fileSystem); | |
| final FileSystem _fileSystem; | |
| final _cache = <String, List<String>>{}; | |
| final _variantsPerFolder = <String, List<File>>{}; | |
| List<String> variantsFor(String assetPath) { | |
| final String directoryName = _fileSystem.path.dirname(assetPath); | |
| try { | |
| if (!_fileSystem.directory(directoryName).existsSync()) { | |
| return const <String>[]; | |
| } | |
| } on FileSystemException catch (e) { | |
| throwToolExit( | |
| 'Unable to check the existence of asset file "$assetPath". ' | |
| 'Ensure that the asset file is declared as a valid local file system path.\n' | |
| 'Details: $e', | |
| ); | |
| } | |
| if (_cache.containsKey(assetPath)) { | |
| return _cache[assetPath]!; | |
| } | |
| if (!_variantsPerFolder.containsKey(directoryName)) { | |
| _variantsPerFolder[directoryName] = _fileSystem | |
| .directory(directoryName) | |
| .listSync() | |
| .whereType<Directory>() | |
| .where((Directory dir) => _assetVariantDirectoryRegExp.hasMatch(dir.basename)) | |
| .expand((Directory dir) => dir.listSync()) | |
| .whereType<File>() | |
| .toList(); | |
| } | |
| final File assetFile = _fileSystem.file(assetPath); | |
| final List<File> potentialVariants = _variantsPerFolder[directoryName]!; | |
| final String basename = assetFile.basename; | |
| return _cache[assetPath] = <String>[ | |
| // It's possible that the user specifies only explicit variants (e.g. .../1x/asset.png), | |
| // so there does not necessarily need to be a file at the given path. | |
| if (assetFile.existsSync()) assetPath, | |
| ...potentialVariants | |
| .where((File file) => file.basename == basename) | |
| .map((File file) => file.path), | |
| ]; | |
| } | |
| } |