From 935775cb74ea1ed567d0a9c2ad61a023034212c6 Mon Sep 17 00:00:00 2001 From: Andrew Kolos Date: Wed, 13 Dec 2023 21:30:10 -0800 Subject: [PATCH] [reland] Support conditional bundling of assets based on `--flavor` (#139834) Reland of https://github.com/flutter/flutter/pull/132985. Fixes the path to AssetManifest.bin in flavors_test_ios --- dev/bots/pubspec.yaml | 3 +- dev/devicelab/bin/tasks/flavors_test.dart | 82 ++++++-- dev/devicelab/bin/tasks/flavors_test_ios.dart | 90 ++++++-- dev/devicelab/pubspec.yaml | 3 +- .../flavors/assets/common/common.txt | 1 + .../flavors/assets/free/free.txt | 1 + .../flavors/assets/paid/paid.txt | 1 + .../flavors/assets/premium/premium.txt | 1 + dev/integration_tests/flavors/pubspec.yaml | 8 + packages/flutter_tools/bin/xcode_backend.dart | 1 + .../gradle/src/main/groovy/flutter.groovy | 7 + packages/flutter_tools/lib/src/asset.dart | 183 ++++++++++++++-- .../lib/src/base/deferred_component.dart | 14 +- .../flutter_tools/lib/src/build_info.dart | 5 + .../lib/src/build_system/targets/android.dart | 2 + .../lib/src/build_system/targets/assets.dart | 3 + .../lib/src/build_system/targets/common.dart | 3 + .../lib/src/build_system/targets/ios.dart | 1 + .../lib/src/build_system/targets/macos.dart | 2 + .../flutter_tools/lib/src/bundle_builder.dart | 2 + .../lib/src/flutter_manifest.dart | 197 +++++++++++++----- packages/flutter_tools/lib/src/run_hot.dart | 7 +- .../commands.shard/hermetic/run_test.dart | 1 + .../test/general.shard/asset_bundle_test.dart | 187 +++++++++++++++++ .../base/deferred_component_test.dart | 26 ++- .../test/general.shard/build_info_test.dart | 5 +- .../build_system/targets/assets_test.dart | 64 ++++++ .../test/general.shard/devfs_test.dart | 9 +- .../general.shard/flutter_manifest_test.dart | 181 ++++++++-------- .../general.shard/xcode_backend_test.dart | 2 + 30 files changed, 880 insertions(+), 212 deletions(-) create mode 100644 dev/integration_tests/flavors/assets/common/common.txt create mode 100644 dev/integration_tests/flavors/assets/free/free.txt create mode 100644 dev/integration_tests/flavors/assets/paid/paid.txt create mode 100644 dev/integration_tests/flavors/assets/premium/premium.txt diff --git a/dev/bots/pubspec.yaml b/dev/bots/pubspec.yaml index 5de06248264..5c47efe86f6 100644 --- a/dev/bots/pubspec.yaml +++ b/dev/bots/pubspec.yaml @@ -58,6 +58,7 @@ dependencies: source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" source_span: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" stack_trace: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + standard_message_codec: 0.0.1+4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" stream_channel: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" string_scanner: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -74,4 +75,4 @@ dependencies: dev_dependencies: test_api: 0.6.1 -# PUBSPEC CHECKSUM: 29d2 +# PUBSPEC CHECKSUM: b875 diff --git a/dev/devicelab/bin/tasks/flavors_test.dart b/dev/devicelab/bin/tasks/flavors_test.dart index 2db0956f79a..e9ebe408248 100644 --- a/dev/devicelab/bin/tasks/flavors_test.dart +++ b/dev/devicelab/bin/tasks/flavors_test.dart @@ -2,12 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:io' show File; +import 'dart:typed_data'; + +import 'package:collection/collection.dart'; import 'package:flutter_devicelab/framework/devices.dart'; import 'package:flutter_devicelab/framework/framework.dart'; import 'package:flutter_devicelab/framework/task_result.dart'; import 'package:flutter_devicelab/framework/utils.dart'; import 'package:flutter_devicelab/tasks/integration_tests.dart'; import 'package:path/path.dart' as path; +import 'package:standard_message_codec/standard_message_codec.dart'; Future main() async { deviceOperatingSystem = DeviceOperatingSystem.android; @@ -15,31 +20,20 @@ Future main() async { await createFlavorsTest().call(); await createIntegrationTestFlavorsTest().call(); + final String projectPath = '${flutterDirectory.path}/dev/integration_tests/flavors'; final TaskResult installTestsResult = await inDirectory( - '${flutterDirectory.path}/dev/integration_tests/flavors', + projectPath, () async { - await flutter( - 'install', - options: ['--debug', '--flavor', 'paid'], - ); - await flutter( - 'install', - options: ['--debug', '--flavor', 'paid', '--uninstall-only'], - ); + final List testResults = [ + await _testInstallDebugPaidFlavor(projectPath), + await _testInstallBogusFlavor(), + ]; - final StringBuffer stderr = StringBuffer(); - await evalFlutter( - 'install', - canFail: true, - stderr: stderr, - options: ['--flavor', 'bogus'], - ); + final TaskResult? firstInstallFailure = testResults + .firstWhereOrNull((TaskResult element) => element.failed); - final String stderrString = stderr.toString(); - final String expectedApkPath = path.join('build', 'app', 'outputs', 'flutter-apk', 'app-bogus-release.apk'); - if (!stderrString.contains('"$expectedApkPath" does not exist.')) { - print(stderrString); - return TaskResult.failure('Should not succeed with bogus flavor'); + if (firstInstallFailure != null) { + return firstInstallFailure; } return TaskResult.success(null); @@ -49,3 +43,49 @@ Future main() async { return installTestsResult; }); } + +// Ensures installation works. Also tests asset bundling while we are at it. +Future _testInstallDebugPaidFlavor(String projectDir) async { + await evalFlutter( + 'install', + options: ['--debug', '--flavor', 'paid'], + ); + + final Uint8List assetManifestFileData = File( + path.join(projectDir, 'build', 'app', 'intermediates', 'assets', 'paidDebug', 'flutter_assets', 'AssetManifest.bin'), + ).readAsBytesSync(); + + final Map assetManifest = const StandardMessageCodec() + .decodeMessage(ByteData.sublistView(assetManifestFileData)) as Map; + + if (assetManifest.containsKey('assets/free/free.txt')) { + return TaskResult.failure('Assets declared with a flavor not equal to the ' + 'argued --flavor value should not be bundled.'); + } + + await flutter( + 'install', + options: ['--debug', '--flavor', 'paid', '--uninstall-only'], + ); + + return TaskResult.success(null); +} + +Future _testInstallBogusFlavor() async { + final StringBuffer stderr = StringBuffer(); + await evalFlutter( + 'install', + canFail: true, + stderr: stderr, + options: ['--flavor', 'bogus'], + ); + + final String stderrString = stderr.toString(); + final String expectedApkPath = path.join('build', 'app', 'outputs', 'flutter-apk', 'app-bogus-release.apk'); + if (!stderrString.contains('"$expectedApkPath" does not exist.')) { + print(stderrString); + return TaskResult.failure('Should not succeed with bogus flavor'); + } + + return TaskResult.success(null); +} diff --git a/dev/devicelab/bin/tasks/flavors_test_ios.dart b/dev/devicelab/bin/tasks/flavors_test_ios.dart index 4e84ed4ec35..9dab73d8687 100644 --- a/dev/devicelab/bin/tasks/flavors_test_ios.dart +++ b/dev/devicelab/bin/tasks/flavors_test_ios.dart @@ -2,11 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:collection/collection.dart'; import 'package:flutter_devicelab/framework/devices.dart'; import 'package:flutter_devicelab/framework/framework.dart'; import 'package:flutter_devicelab/framework/task_result.dart'; import 'package:flutter_devicelab/framework/utils.dart'; import 'package:flutter_devicelab/tasks/integration_tests.dart'; +import 'package:path/path.dart' as path; +import 'package:standard_message_codec/standard_message_codec.dart'; Future main() async { deviceOperatingSystem = DeviceOperatingSystem.ios; @@ -14,29 +20,20 @@ Future main() async { await createFlavorsTest().call(); await createIntegrationTestFlavorsTest().call(); // test install and uninstall of flavors app + final String projectDir = '${flutterDirectory.path}/dev/integration_tests/flavors'; final TaskResult installTestsResult = await inDirectory( - '${flutterDirectory.path}/dev/integration_tests/flavors', + projectDir, () async { - await flutter( - 'install', - options: ['--flavor', 'paid'], - ); - await flutter( - 'install', - options: ['--flavor', 'paid', '--uninstall-only'], - ); - final StringBuffer stderr = StringBuffer(); - await evalFlutter( - 'install', - canFail: true, - stderr: stderr, - options: ['--flavor', 'bogus'], - ); + final List testResults = [ + await _testInstallDebugPaidFlavor(projectDir), + await _testInstallBogusFlavor(), + ]; - final String stderrString = stderr.toString(); - if (!stderrString.contains('The Xcode project defines schemes: free, paid')) { - print(stderrString); - return TaskResult.failure('Should not succeed with bogus flavor'); + final TaskResult? firstInstallFailure = testResults + .firstWhereOrNull((TaskResult element) => element.failed); + + if (firstInstallFailure != null) { + return firstInstallFailure; } return TaskResult.success(null); @@ -46,3 +43,56 @@ Future main() async { return installTestsResult; }); } + +Future _testInstallDebugPaidFlavor(String projectDir) async { + await evalFlutter( + 'install', + options: ['--flavor', 'paid'], + ); + final Uint8List assetManifestFileData = File( + path.join( + projectDir, + 'build', + 'ios', + 'iphoneos', + 'Paid App.app', + 'Frameworks', + 'App.framework', + 'flutter_assets', + 'AssetManifest.bin', + ), + ).readAsBytesSync(); + + final Map assetManifest = const StandardMessageCodec() + .decodeMessage(ByteData.sublistView(assetManifestFileData)) as Map; + + if (assetManifest.containsKey('assets/free/free.txt')) { + return TaskResult.failure('Assets declared with a flavor not equal to the ' + 'argued --flavor value should not be bundled.'); + } + + await flutter( + 'install', + options: ['--flavor', 'paid', '--uninstall-only'], + ); + + return TaskResult.success(null); +} + +Future _testInstallBogusFlavor() async { + final StringBuffer stderr = StringBuffer(); + await evalFlutter( + 'install', + canFail: true, + stderr: stderr, + options: ['--flavor', 'bogus'], + ); + + final String stderrString = stderr.toString(); + if (!stderrString.contains('The Xcode project defines schemes: free, paid')) { + print(stderrString); + return TaskResult.failure('Should not succeed with bogus flavor'); + } + + return TaskResult.success(null); +} diff --git a/dev/devicelab/pubspec.yaml b/dev/devicelab/pubspec.yaml index e9387801385..ed834b7a61b 100644 --- a/dev/devicelab/pubspec.yaml +++ b/dev/devicelab/pubspec.yaml @@ -24,6 +24,7 @@ dependencies: web: 0.4.0 webkit_inspection_protocol: 1.2.1 xml: 6.5.0 + standard_message_codec: 0.0.1+4 _discoveryapis_commons: 1.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -72,4 +73,4 @@ dev_dependencies: watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: 6040 +# PUBSPEC CHECKSUM: 70e2 diff --git a/dev/integration_tests/flavors/assets/common/common.txt b/dev/integration_tests/flavors/assets/common/common.txt new file mode 100644 index 00000000000..b804e9886ab --- /dev/null +++ b/dev/integration_tests/flavors/assets/common/common.txt @@ -0,0 +1 @@ +this is a test asset not meant for any specific flavor \ No newline at end of file diff --git a/dev/integration_tests/flavors/assets/free/free.txt b/dev/integration_tests/flavors/assets/free/free.txt new file mode 100644 index 00000000000..8f7a269f728 --- /dev/null +++ b/dev/integration_tests/flavors/assets/free/free.txt @@ -0,0 +1 @@ +this is a test asset for --flavor free \ No newline at end of file diff --git a/dev/integration_tests/flavors/assets/paid/paid.txt b/dev/integration_tests/flavors/assets/paid/paid.txt new file mode 100644 index 00000000000..c6b999fd68e --- /dev/null +++ b/dev/integration_tests/flavors/assets/paid/paid.txt @@ -0,0 +1 @@ +this is a test asset for --flavor paid \ No newline at end of file diff --git a/dev/integration_tests/flavors/assets/premium/premium.txt b/dev/integration_tests/flavors/assets/premium/premium.txt new file mode 100644 index 00000000000..f7902452189 --- /dev/null +++ b/dev/integration_tests/flavors/assets/premium/premium.txt @@ -0,0 +1 @@ +premium \ No newline at end of file diff --git a/dev/integration_tests/flavors/pubspec.yaml b/dev/integration_tests/flavors/pubspec.yaml index 6b05736c805..d0c979db4eb 100644 --- a/dev/integration_tests/flavors/pubspec.yaml +++ b/dev/integration_tests/flavors/pubspec.yaml @@ -75,5 +75,13 @@ dev_dependencies: flutter: uses-material-design: true + assets: + - assets/common/common.txt + - path: assets/paid/ + flavors: + - paid + - path: assets/free/ + flavors: + - free # PUBSPEC CHECKSUM: 6bd3 diff --git a/packages/flutter_tools/bin/xcode_backend.dart b/packages/flutter_tools/bin/xcode_backend.dart index 70e856405e9..dca13dad5f7 100644 --- a/packages/flutter_tools/bin/xcode_backend.dart +++ b/packages/flutter_tools/bin/xcode_backend.dart @@ -401,6 +401,7 @@ class Context { '-dTargetPlatform=ios', '-dTargetFile=$targetPath', '-dBuildMode=$buildMode', + if (environment['FLAVOR'] != null) '-dFlavor=${environment['FLAVOR']}', '-dIosArchs=${environment['ARCHS'] ?? ''}', '-dSdkRoot=${environment['SDKROOT'] ?? ''}', '-dSplitDebugInfo=${environment['SPLIT_DEBUG_INFO'] ?? ''}', diff --git a/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy b/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy index 5562bbbe40b..3e3bb0f90ad 100644 --- a/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy +++ b/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy @@ -1057,6 +1057,7 @@ class FlutterPlugin implements Plugin { boolean isAndroidLibraryValue = isBuildingAar || isUsedAsSubproject String variantBuildMode = buildModeFor(variant.buildType) + String flavorValue = variant.getFlavorName() String taskName = toCamelCase(["compile", FLUTTER_BUILD_PREFIX, variant.name]) // Be careful when configuring task below, Groovy has bizarre // scoping rules: writing `verbose isVerbose()` means calling @@ -1094,6 +1095,7 @@ class FlutterPlugin implements Plugin { deferredComponents deferredComponentsValue validateDeferredComponents validateDeferredComponentsValue isAndroidLibrary isAndroidLibraryValue + flavor flavorValue } File libJar = project.file("${project.buildDir}/$INTERMEDIATES_DIR/flutter/${variant.name}/libs.jar") Task packFlutterAppAotTask = project.tasks.create(name: "packLibs${FLUTTER_BUILD_PREFIX}${variant.name.capitalize()}", type: Jar) { @@ -1380,6 +1382,8 @@ abstract class BaseFlutterTask extends DefaultTask { Boolean validateDeferredComponents @Optional @Input Boolean isAndroidLibrary + @Optional @Input + String flavor @OutputFiles FileCollection getDependenciesFiles() { @@ -1460,6 +1464,9 @@ abstract class BaseFlutterTask extends DefaultTask { if (codeSizeDirectory != null) { args "-dCodeSizeDirectory=${codeSizeDirectory}" } + if (flavor != null) { + args "-dFlavor=${flavor}" + } if (extraGenSnapshotOptions != null) { args "--ExtraGenSnapshotOptions=${extraGenSnapshotOptions}" } diff --git a/packages/flutter_tools/lib/src/asset.dart b/packages/flutter_tools/lib/src/asset.dart index 7eda50bc1f7..9d936156d5f 100644 --- a/packages/flutter_tools/lib/src/asset.dart +++ b/packages/flutter_tools/lib/src/asset.dart @@ -8,6 +8,7 @@ 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'; @@ -102,6 +103,7 @@ abstract class AssetBundle { required String packagesPath, bool deferredComponentsEnabled = false, TargetPlatform? targetPlatform, + String? flavor, }); } @@ -216,8 +218,8 @@ class ManifestAssetBundle implements AssetBundle { required String packagesPath, bool deferredComponentsEnabled = false, TargetPlatform? targetPlatform, + String? flavor, }) async { - if (flutterProject == null) { try { flutterProject = FlutterProject.fromDirectory(_fileSystem.file(manifestPath).parent); @@ -268,6 +270,7 @@ class ManifestAssetBundle implements AssetBundle { wildcardDirectories, assetBasePath, targetPlatform, + flavor: flavor, ); if (assetVariants == null) { @@ -281,6 +284,7 @@ class ManifestAssetBundle implements AssetBundle { assetBasePath, wildcardDirectories, flutterProject.directory, + flavor: flavor, ); if (!_splitDeferredAssets || !deferredComponentsEnabled) { // Include the assets in the regular set of assets if not using deferred @@ -621,7 +625,7 @@ class ManifestAssetBundle implements AssetBundle { String assetBasePath, List wildcardDirectories, Directory projectDirectory, { - List excludeDirs = const [], + String? flavor, }) { final List? components = flutterManifest.deferredComponents; final Map>> deferredComponentsAssetVariants = >>{}; @@ -629,18 +633,18 @@ class ManifestAssetBundle implements AssetBundle { return deferredComponentsAssetVariants; } for (final DeferredComponent component in components) { - deferredComponentsAssetVariants[component.name] = <_Asset, List<_Asset>>{}; final _AssetDirectoryCache cache = _AssetDirectoryCache(_fileSystem); - for (final Uri assetUri in component.assets) { - if (assetUri.path.endsWith('/')) { - wildcardDirectories.add(assetUri); + final Map<_Asset, List<_Asset>> 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, - deferredComponentsAssetVariants[component.name]!, - assetUri, + componentAssets, + assetsEntry.uri, ); } else { _parseAssetFromFile( @@ -648,12 +652,14 @@ class ManifestAssetBundle implements AssetBundle { flutterManifest, assetBasePath, cache, - deferredComponentsAssetVariants[component.name]!, - assetUri, - excludeDirs: excludeDirs, + componentAssets, + assetsEntry.uri, ); } } + + componentAssets.removeWhere((_Asset asset, List<_Asset> variants) => !asset.matchesFlavor(flavor)); + deferredComponentsAssetVariants[component.name] = componentAssets; } return deferredComponentsAssetVariants; } @@ -800,22 +806,24 @@ class ManifestAssetBundle implements AssetBundle { TargetPlatform? targetPlatform, { String? packageName, Package? attributedPackage, + String? flavor, }) { final Map<_Asset, List<_Asset>> result = <_Asset, List<_Asset>>{}; final _AssetDirectoryCache cache = _AssetDirectoryCache(_fileSystem); - for (final Uri assetUri in flutterManifest.assets) { - if (assetUri.path.endsWith('/')) { - wildcardDirectories.add(assetUri); + for (final AssetsEntry assetsEntry in flutterManifest.assets) { + if (assetsEntry.uri.path.endsWith('/')) { + wildcardDirectories.add(assetsEntry.uri); _parseAssetsFromFolder( packageConfig, flutterManifest, assetBase, cache, result, - assetUri, + assetsEntry.uri, packageName: packageName, attributedPackage: attributedPackage, + flavors: assetsEntry.flavors, ); } else { _parseAssetFromFile( @@ -824,13 +832,24 @@ class ManifestAssetBundle implements AssetBundle { assetBase, cache, result, - assetUri, + assetsEntry.uri, packageName: packageName, attributedPackage: attributedPackage, + flavors: assetsEntry.flavors, ); } } + 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; + } + return false; + }); + for (final Uri shaderUri in flutterManifest.shaders) { _parseAssetFromFile( packageConfig, @@ -878,6 +897,7 @@ class ManifestAssetBundle implements AssetBundle { result[baseAsset] = <_Asset>[]; } } + return result; } @@ -890,6 +910,7 @@ class ManifestAssetBundle implements AssetBundle { Uri assetUri, { String? packageName, Package? attributedPackage, + List? flavors, }) { final String directoryPath = _fileSystem.path.join( assetBase, assetUri.toFilePath(windows: _platform.isWindows)); @@ -915,6 +936,8 @@ class ManifestAssetBundle implements AssetBundle { uri, packageName: packageName, attributedPackage: attributedPackage, + originUri: assetUri, + flavors: flavors, ); } } @@ -926,10 +949,11 @@ class ManifestAssetBundle implements AssetBundle { _AssetDirectoryCache cache, Map<_Asset, List<_Asset>> result, Uri assetUri, { - List excludeDirs = const [], + Uri? originUri, String? packageName, Package? attributedPackage, AssetKind assetKind = AssetKind.regular, + List? flavors, }) { final _Asset asset = _resolveAsset( packageConfig, @@ -938,9 +962,15 @@ class ManifestAssetBundle implements AssetBundle { packageName, attributedPackage, assetKind: assetKind, + originUri: originUri, + flavors: flavors, ); + + _checkForFlavorConflicts(asset, result.keys.toList()); + final List<_Asset> variants = <_Asset>[]; final File assetFile = asset.lookupAssetFile(_fileSystem); + for (final String path in cache.variantsFor(assetFile.path)) { final String relativePath = _fileSystem.path.relative(path, from: asset.baseDir); final Uri relativeUri = _fileSystem.path.toUri(relativePath); @@ -963,13 +993,83 @@ class ManifestAssetBundle implements AssetBundle { 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 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 StringBuffer 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()); + } + _Asset _resolveAsset( PackageConfig packageConfig, String assetsBaseDir, Uri assetUri, String? packageName, Package? attributedPackage, { + Uri? originUri, AssetKind assetKind = AssetKind.regular, + List? flavors, }) { final String assetPath = _fileSystem.path.fromUri(assetUri); if (assetUri.pathSegments.first == 'packages' @@ -981,6 +1081,8 @@ class ManifestAssetBundle implements AssetBundle { packageConfig, attributedPackage, assetKind: assetKind, + originUri: originUri, + flavors: flavors, ); if (packageAsset != null) { return packageAsset; @@ -994,7 +1096,9 @@ class ManifestAssetBundle implements AssetBundle { : Uri(pathSegments: ['packages', packageName, ...assetUri.pathSegments]), // Asset from, and declared in $packageName. relativeUri: assetUri, package: attributedPackage, + originUri: originUri, assetKind: assetKind, + flavors: flavors, ); } @@ -1003,6 +1107,8 @@ class ManifestAssetBundle implements AssetBundle { PackageConfig packageConfig, Package? attributedPackage, { AssetKind assetKind = AssetKind.regular, + Uri? originUri, + List? flavors, }) { assert(assetUri.pathSegments.first == 'packages'); if (assetUri.pathSegments.length > 1) { @@ -1016,6 +1122,8 @@ class ManifestAssetBundle implements AssetBundle { relativeUri: Uri(pathSegments: assetUri.pathSegments.sublist(2)), package: attributedPackage, assetKind: assetKind, + originUri: originUri, + flavors: flavors, ); } } @@ -1032,16 +1140,22 @@ class ManifestAssetBundle implements AssetBundle { class _Asset { const _Asset({ required this.baseDir, + Uri? originUri, required this.relativeUri, required this.entryUri, required this.package, this.assetKind = AssetKind.regular, - }); + List? flavors, + }): originUri = originUri ?? entryUri, flavors = flavors ?? const []; 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; @@ -1051,6 +1165,8 @@ class _Asset { final AssetKind assetKind; + final List flavors; + File lookupAssetFile(FileSystem fileSystem) { return fileSystem.file(fileSystem.path.join(baseDir, fileSystem.path.fromUri(relativeUri))); } @@ -1065,6 +1181,26 @@ class _Asset { 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 hasEquivalentFlavorsWith(_Asset other) { + final Set assetFlavors = flavors.toSet(); + final Set otherFlavors = other.flavors.toSet(); + return assetFlavors.length == otherFlavors.length && assetFlavors.every( + (String e) => otherFlavors.contains(e), + ); + } + @override String toString() => 'asset: $entryUri'; @@ -1080,11 +1216,18 @@ class _Asset { && other.baseDir == baseDir && other.relativeUri == relativeUri && other.entryUri == entryUri - && other.assetKind == assetKind; + && other.assetKind == assetKind + && hasEquivalentFlavorsWith(other); } @override - int get hashCode => Object.hash(baseDir, relativeUri, entryUri.hashCode); + int get hashCode => Object.hashAll([ + baseDir, + relativeUri, + entryUri, + assetKind, + ...flavors, + ]); } // Given an assets directory like this: diff --git a/packages/flutter_tools/lib/src/base/deferred_component.dart b/packages/flutter_tools/lib/src/base/deferred_component.dart index 748b0086c21..6bf2219e9e7 100644 --- a/packages/flutter_tools/lib/src/base/deferred_component.dart +++ b/packages/flutter_tools/lib/src/base/deferred_component.dart @@ -5,6 +5,7 @@ import '../base/file_system.dart'; import '../base/logger.dart'; import '../convert.dart'; +import '../flutter_manifest.dart'; /// Represents a configured deferred component as defined in /// the app's pubspec.yaml. @@ -12,7 +13,7 @@ class DeferredComponent { DeferredComponent({ required this.name, this.libraries = const [], - this.assets = const [], + this.assets = const [], }) : _assigned = false; /// The name of the deferred component. There should be a matching @@ -28,8 +29,8 @@ class DeferredComponent { /// libraries that are not listed here. final List libraries; - /// Assets that are part of this component as a Uri relative to the project directory. - final List assets; + /// Assets that are part of this component. + final List assets; /// The minimal set of [LoadingUnit]s needed that contain all of the dart libraries in /// [libraries]. @@ -95,8 +96,11 @@ class DeferredComponent { } } out.write('\n Assets:'); - for (final Uri asset in assets) { - out.write('\n - ${asset.path}'); + for (final AssetsEntry asset in assets) { + out.write('\n - ${asset.uri.path}'); + if (asset.flavors.isNotEmpty) { + out.write(' (flavors: ${asset.flavors.join(', ')})'); + } } return out.toString(); } diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart index 825b0df74ad..b42edc1d51f 100644 --- a/packages/flutter_tools/lib/src/build_info.dart +++ b/packages/flutter_tools/lib/src/build_info.dart @@ -286,6 +286,8 @@ class BuildInfo { 'PACKAGE_CONFIG': packagesPath, if (codeSizeDirectory != null) 'CODE_SIZE_DIRECTORY': codeSizeDirectory!, + if (flavor != null) + 'FLAVOR': flavor!, }; } @@ -989,6 +991,9 @@ const String kBundleSkSLPath = 'BundleSkSLPath'; /// The define to pass build name const String kBuildName = 'BuildName'; +/// The app flavor to build. +const String kFlavor = 'Flavor'; + /// The define to pass build number const String kBuildNumber = 'BuildNumber'; diff --git a/packages/flutter_tools/lib/src/build_system/targets/android.dart b/packages/flutter_tools/lib/src/build_system/targets/android.dart index 1c8e8fb7855..8254234de27 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/android.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/android.dart @@ -46,6 +46,7 @@ abstract class AndroidAssetBundle extends Target { if (buildModeEnvironment == null) { throw MissingDefineException(kBuildMode, name); } + final BuildMode buildMode = BuildMode.fromCliName(buildModeEnvironment); final Directory outputDirectory = environment.outputDir .childDirectory('flutter_assets') @@ -68,6 +69,7 @@ abstract class AndroidAssetBundle extends Target { targetPlatform: TargetPlatform.android, buildMode: buildMode, shaderTarget: ShaderTarget.impellerAndroid, + flavor: environment.defines[kFlavor], ); environment.depFileService.writeToFile( assetDepfile, diff --git a/packages/flutter_tools/lib/src/build_system/targets/assets.dart b/packages/flutter_tools/lib/src/build_system/targets/assets.dart index dc9c9aff910..6ad00d1ebad 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/assets.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/assets.dart @@ -34,6 +34,7 @@ Future copyAssets( BuildMode? buildMode, required ShaderTarget shaderTarget, List additionalInputs = const [], + String? flavor, }) async { // Check for an SkSL bundle. final String? shaderBundlePath = environment.defines[kBundleSkSLPath] ?? environment.inputs[kBundleSkSLPath]; @@ -58,6 +59,7 @@ Future copyAssets( packagesPath: environment.projectDir.childFile('.packages').path, deferredComponentsEnabled: environment.defines[kDeferredComponents] == 'true', targetPlatform: targetPlatform, + flavor: flavor, ); if (resultCode != 0) { throw Exception('Failed to bundle asset files.'); @@ -323,6 +325,7 @@ class CopyAssets extends Target { output, targetPlatform: TargetPlatform.android, shaderTarget: ShaderTarget.sksl, + flavor: environment.defines[kFlavor], ); environment.depFileService.writeToFile( depfile, diff --git a/packages/flutter_tools/lib/src/build_system/targets/common.dart b/packages/flutter_tools/lib/src/build_system/targets/common.dart index c3507abb349..461638298ba 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/common.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/common.dart @@ -58,6 +58,8 @@ class CopyFlutterBundle extends Target { if (buildModeEnvironment == null) { throw MissingDefineException(kBuildMode, 'copy_flutter_bundle'); } + final String? flavor = environment.defines[kFlavor]; + final BuildMode buildMode = BuildMode.fromCliName(buildModeEnvironment); environment.outputDir.createSync(recursive: true); @@ -78,6 +80,7 @@ class CopyFlutterBundle extends Target { targetPlatform: TargetPlatform.android, buildMode: buildMode, shaderTarget: ShaderTarget.sksl, + flavor: flavor, ); environment.depFileService.writeToFile( assetDepfile, diff --git a/packages/flutter_tools/lib/src/build_system/targets/ios.dart b/packages/flutter_tools/lib/src/build_system/targets/ios.dart index 66c806b60c3..9336cd74151 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/ios.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/ios.dart @@ -533,6 +533,7 @@ abstract class IosAssetBundle extends Target { flutterProject.ios.infoPlist, flutterProject.ios.appFrameworkInfoPlist, ], + flavor: environment.defines[kFlavor], ); environment.depFileService.writeToFile( assetDepfile, diff --git a/packages/flutter_tools/lib/src/build_system/targets/macos.dart b/packages/flutter_tools/lib/src/build_system/targets/macos.dart index 85bd01a9e58..7cd1e8193d7 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/macos.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/macos.dart @@ -393,6 +393,7 @@ abstract class MacOSBundleFlutterAssets extends Target { if (buildModeEnvironment == null) { throw MissingDefineException(kBuildMode, 'compile_macos_framework'); } + final BuildMode buildMode = BuildMode.fromCliName(buildModeEnvironment); final Directory frameworkRootDirectory = environment .outputDir @@ -439,6 +440,7 @@ abstract class MacOSBundleFlutterAssets extends Target { assetDirectory, targetPlatform: TargetPlatform.darwin, shaderTarget: ShaderTarget.sksl, + flavor: environment.defines[kFlavor], ); environment.depFileService.writeToFile( assetDepfile, diff --git a/packages/flutter_tools/lib/src/bundle_builder.dart b/packages/flutter_tools/lib/src/bundle_builder.dart index f1ec2b13573..496f2d0138e 100644 --- a/packages/flutter_tools/lib/src/bundle_builder.dart +++ b/packages/flutter_tools/lib/src/bundle_builder.dart @@ -114,6 +114,7 @@ Future buildAssets({ String? assetDirPath, String? packagesPath, TargetPlatform? targetPlatform, + String? flavor, }) async { assetDirPath ??= getAssetBuildDirectory(); packagesPath ??= globals.fs.path.absolute('.packages'); @@ -124,6 +125,7 @@ Future buildAssets({ manifestPath: manifestPath, packagesPath: packagesPath, targetPlatform: targetPlatform, + flavor: flavor, ); if (result != 0) { return null; diff --git a/packages/flutter_tools/lib/src/flutter_manifest.dart b/packages/flutter_tools/lib/src/flutter_manifest.dart index 68554d6ce95..473413982b8 100644 --- a/packages/flutter_tools/lib/src/flutter_manifest.dart +++ b/packages/flutter_tools/lib/src/flutter_manifest.dart @@ -231,29 +231,12 @@ class FlutterManifest { _logger.printError('Expected deferred component manifest to be a map.'); continue; } - List assetsUri = []; - final List? assets = component['assets'] as List?; - if (assets == null) { - assetsUri = const []; - } else { - for (final Object? asset in assets) { - if (asset is! String || asset == '') { - _logger.printError('Deferred component asset manifest contains a null or empty uri.'); - continue; - } - try { - assetsUri.add(Uri.parse(asset)); - } on FormatException { - _logger.printError('Asset manifest contains invalid uri: $asset.'); - } - } - } components.add( DeferredComponent( name: component['name'] as String, libraries: component['libraries'] == null ? [] : (component['libraries'] as List).cast(), - assets: assetsUri, + assets: _computeAssets(component['assets']), ) ); } @@ -311,26 +294,7 @@ class FlutterManifest { : fontList.map?>(castStringKeyedMap).whereType>().toList(); } - late final List assets = _computeAssets(); - List _computeAssets() { - final List? assets = _flutterDescriptor['assets'] as List?; - if (assets == null) { - return const []; - } - final List results = []; - for (final Object? asset in assets) { - if (asset is! String || asset == '') { - _logger.printError('Asset manifest contains a null or empty uri.'); - continue; - } - try { - results.add(Uri(pathSegments: asset.split('/'))); - } on FormatException { - _logger.printError('Asset manifest contains invalid uri: $asset.'); - } - } - return results; - } + late final List assets = _computeAssets(_flutterDescriptor['assets']); late final List fonts = _extractFonts(); @@ -521,15 +485,7 @@ void _validateFlutter(YamlMap? yaml, List errors) { errors.add('Expected "$yamlKey" to be a bool, but got $yamlValue (${yamlValue.runtimeType}).'); } case 'assets': - if (yamlValue is! YamlList) { - errors.add('Expected "$yamlKey" to be a list, but got $yamlValue (${yamlValue.runtimeType}).'); - } else if (yamlValue.isEmpty) { - break; - } else if (yamlValue[0] is! String) { - errors.add( - 'Expected "$yamlKey" to be a list of strings, but the first element is $yamlValue (${yamlValue.runtimeType}).', - ); - } + errors.addAll(_validateAssets(yamlValue)); case 'shaders': if (yamlValue is! YamlList) { errors.add('Expected "$yamlKey" to be a list, but got $yamlValue (${yamlValue.runtimeType}).'); @@ -640,17 +596,52 @@ void _validateDeferredComponents(MapEntry kvp, List er } } if (valueMap.containsKey('assets')) { - final Object? assets = valueMap['assets']; - if (assets is! YamlList) { - errors.add('Expected "assets" to be a list, but got $assets (${assets.runtimeType}).'); - } else { - _validateListType(assets, errors, '"assets" key in the $i element of "${kvp.key}"', 'file paths'); - } + errors.addAll(_validateAssets(valueMap['assets'])); } } } } +List _validateAssets(Object? yaml) { + final (_, List errors) = _computeAssetsSafe(yaml); + return errors; +} + +// TODO(andrewkolos): We end up parsing the assets section twice, once during +// validation and once when the assets getter is called. We should consider +// refactoring this class to parse and store everything in the constructor. +// https://github.com/flutter/flutter/issues/139183 +(List, List errors) _computeAssetsSafe(Object? yaml) { + if (yaml == null) { + return (const [], const []); + } + if (yaml is! YamlList) { + final String error = 'Expected "assets" to be a list, but got $yaml (${yaml.runtimeType}).'; + return (const [], [error]); + } + final List results = []; + final List errors = []; + for (final Object? rawAssetEntry in yaml) { + final (AssetsEntry? parsed, String? error) = AssetsEntry.parseFromYamlSafe(rawAssetEntry); + if (parsed != null) { + results.add(parsed); + } + if (error != null) { + errors.add(error); + } + } + return (results, errors); +} + +List _computeAssets(Object? assetsSection) { + final (List result, List errors) = _computeAssetsSafe(assetsSection); + if (errors.isNotEmpty) { + throw Exception('Uncaught error(s) in assets section: ' + '${errors.join('\n')}'); + } + return result; +} + void _validateFonts(YamlList fonts, List errors) { const Set fontWeights = { 100, 200, 300, 400, 500, 600, 700, 800, 900, @@ -703,3 +694,103 @@ void _validateFonts(YamlList fonts, List errors) { } } } + +/// Represents an entry under the `assets` section of a pubspec. +@immutable +class AssetsEntry { + const AssetsEntry({ + required this.uri, + this.flavors = const [], + }); + + final Uri uri; + final List flavors; + + static const String _pathKey = 'path'; + static const String _flavorKey = 'flavors'; + + static AssetsEntry? parseFromYaml(Object? yaml) { + final (AssetsEntry? value, String? error) = parseFromYamlSafe(yaml); + if (error != null) { + throw Exception('Unexpected error when parsing assets entry'); + } + return value!; + } + + static (AssetsEntry? assetsEntry, String? error) parseFromYamlSafe(Object? yaml) { + + (Uri?, String?) tryParseUri(String uri) { + try { + return (Uri(pathSegments: uri.split('/')), null); + } on FormatException { + return (null, 'Asset manifest contains invalid uri: $uri.'); + } + } + + if (yaml == null || yaml == '') { + return (null, 'Asset manifest contains a null or empty uri.'); + } + + if (yaml is String) { + final (Uri? uri, String? error) = tryParseUri(yaml); + return uri == null ? (null, error) : (AssetsEntry(uri: uri), null); + } + + if (yaml is Map) { + if (yaml.keys.isEmpty) { + return (null, null); + } + + final Object? path = yaml[_pathKey]; + final Object? flavors = yaml[_flavorKey]; + + if (path == null || path is! String) { + return (null, 'Asset manifest entry is malformed. ' + 'Expected asset entry to be either a string or a map ' + 'containing a "$_pathKey" entry. Got ${path.runtimeType} instead.'); + } + + final Uri uri = Uri(pathSegments: path.split('/')); + + if (flavors == null) { + return (AssetsEntry(uri: uri), null); + } + + if (flavors is! YamlList) { + return(null, 'Asset manifest entry is malformed. ' + 'Expected "$_flavorKey" entry to be a list of strings. ' + 'Got ${flavors.runtimeType} instead.'); + } + + final List flavorsListErrors = []; + _validateListType(flavors, flavorsListErrors, 'flavors list of entry "$path"', 'String'); + if (flavorsListErrors.isNotEmpty) { + return (null, 'Asset manifest entry is malformed. ' + 'Expected "$_flavorKey" entry to be a list of strings.\n' + '${flavorsListErrors.join('\n')}'); + } + + final AssetsEntry entry = AssetsEntry( + uri: Uri(pathSegments: path.split('/')), + flavors: List.from(flavors), + ); + + return (entry, null); + } + + return (null, 'Assets entry had unexpected shape. ' + 'Expected a string or an object. Got ${yaml.runtimeType} instead.'); + } + + @override + bool operator ==(Object other) { + if (other is! AssetsEntry) { + return false; + } + + return uri == other.uri && flavors == other.flavors; + } + + @override + int get hashCode => Object.hash(uri.hashCode, flavors.hashCode); +} diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart index 117e0f389c7..740b18961e7 100644 --- a/packages/flutter_tools/lib/src/run_hot.dart +++ b/packages/flutter_tools/lib/src/run_hot.dart @@ -141,6 +141,8 @@ class HotRunner extends ResidentRunner { NativeAssetsBuildRunner? _buildRunner; + String? flavor; + Future _calculateTargetPlatform() async { if (_targetPlatform != null) { return; @@ -494,7 +496,10 @@ class HotRunner extends ResidentRunner { final bool rebuildBundle = assetBundle.needsBuild(); if (rebuildBundle) { globals.printTrace('Updating assets'); - final int result = await assetBundle.build(packagesPath: '.packages'); + final int result = await assetBundle.build( + packagesPath: '.packages', + flavor: debuggingOptions.buildInfo.flavor, + ); if (result != 0) { return UpdateFSReport(); } diff --git a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart index 3032eafd9a0..6aef5d2ebb4 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart @@ -1567,6 +1567,7 @@ class CapturingAppDomain extends AppDomain { bool machine = true, String? userIdentifier, bool enableDevTools = true, + String? flavor, }) async { this.multidexEnabled = multidexEnabled; this.userIdentifier = userIdentifier; diff --git a/packages/flutter_tools/test/general.shard/asset_bundle_test.dart b/packages/flutter_tools/test/general.shard/asset_bundle_test.dart index 4b798f61ec6..3704f1bb08b 100644 --- a/packages/flutter_tools/test/general.shard/asset_bundle_test.dart +++ b/packages/flutter_tools/test/general.shard/asset_bundle_test.dart @@ -9,11 +9,15 @@ import 'package:file/memory.dart'; import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/asset.dart'; import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/base/user_messages.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/bundle_builder.dart'; +import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/globals.dart' as globals; +import 'package:flutter_tools/src/project.dart'; import 'package:standard_message_codec/standard_message_codec.dart'; import '../src/common.dart'; @@ -23,7 +27,9 @@ const String shaderLibDir = '/./shader_lib'; void main() { group('AssetBundle.build', () { + late Logger logger; late FileSystem testFileSystem; + late Platform platform; setUp(() async { testFileSystem = MemoryFileSystem( @@ -32,6 +38,8 @@ void main() { : FileSystemStyle.posix, ); testFileSystem.currentDirectory = testFileSystem.systemTempDirectory.createTempSync('flutter_asset_bundle_test.'); + logger = BufferLogger.test(); + platform = FakePlatform(operatingSystem: globals.platform.operatingSystem); }); testUsingContext('nonempty', () async { @@ -323,6 +331,185 @@ flutter: FileSystem: () => testFileSystem, ProcessManager: () => FakeProcessManager.any(), }); + + group('flavors feature', () { + Future buildBundleWithFlavor(String? flavor) async { + final ManifestAssetBundle bundle = ManifestAssetBundle( + logger: logger, + fileSystem: testFileSystem, + platform: platform, + splitDeferredAssets: true, + ); + + await bundle.build( + packagesPath: '.packages', + flutterProject: FlutterProject.fromDirectoryTest(testFileSystem.currentDirectory), + flavor: flavor, + ); + return bundle; + } + + late final String? previousCacheFlutterRootValue; + + setUpAll(() { + previousCacheFlutterRootValue = Cache.flutterRoot; + Cache.flutterRoot = Cache.defaultFlutterRoot(platform: platform, fileSystem: testFileSystem, userMessages: UserMessages()); + }); + + tearDownAll(() => Cache.flutterRoot = previousCacheFlutterRootValue); + + testWithoutContext('correctly bundles assets given a simple asset manifest with flavors', () async { + testFileSystem.file('.packages').createSync(); + testFileSystem.file(testFileSystem.path.join('assets', 'common', 'image.png')).createSync(recursive: true); + testFileSystem.file(testFileSystem.path.join('assets', 'vanilla', 'ice-cream.png')).createSync(recursive: true); + testFileSystem.file(testFileSystem.path.join('assets', 'strawberry', 'ice-cream.png')).createSync(recursive: true); + testFileSystem.file(testFileSystem.path.join('assets', 'orange', 'ice-cream.png')).createSync(recursive: true); + testFileSystem.file('pubspec.yaml') + ..createSync() + ..writeAsStringSync(r''' +name: example +flutter: + assets: + - assets/common/ + - path: assets/vanilla/ + flavors: + - vanilla + - path: assets/strawberry/ + flavors: + - strawberry + - path: assets/orange/ice-cream.png + flavors: + - orange + '''); + + ManifestAssetBundle bundle; + bundle = await buildBundleWithFlavor(null); + expect(bundle.entries.keys, contains('assets/common/image.png')); + expect(bundle.entries.keys, isNot(contains('assets/vanilla/ice-cream.png'))); + expect(bundle.entries.keys, isNot(contains('assets/strawberry/ice-cream.png'))); + expect(bundle.entries.keys, isNot(contains('assets/orange/ice-cream.png'))); + + bundle = await buildBundleWithFlavor('strawberry'); + expect(bundle.entries.keys, contains('assets/common/image.png')); + expect(bundle.entries.keys, isNot(contains('assets/vanilla/ice-cream.png'))); + expect(bundle.entries.keys, contains('assets/strawberry/ice-cream.png')); + expect(bundle.entries.keys, isNot(contains('assets/orange/ice-cream.png'))); + + bundle = await buildBundleWithFlavor('orange'); + expect(bundle.entries.keys, contains('assets/common/image.png')); + expect(bundle.entries.keys, isNot(contains('assets/vanilla/ice-cream.png'))); + expect(bundle.entries.keys, isNot(contains('assets/strawberry/ice-cream.png'))); + expect(bundle.entries.keys, contains('assets/orange/ice-cream.png')); + }); + + testWithoutContext('throws a tool exit when a non-flavored folder contains a flavored asset', () async { + testFileSystem.file('.packages').createSync(); + testFileSystem.file(testFileSystem.path.join('assets', 'unflavored.png')).createSync(recursive: true); + testFileSystem.file(testFileSystem.path.join('assets', 'vanillaOrange.png')).createSync(recursive: true); + + testFileSystem.file('pubspec.yaml') + ..createSync() + ..writeAsStringSync(r''' + name: example + flutter: + assets: + - assets/ + - path: assets/vanillaOrange.png + flavors: + - vanilla + - orange + '''); + + expect( + buildBundleWithFlavor(null), + throwsToolExit(message: 'Multiple assets entries include the file ' + '"assets/vanillaOrange.png", but they specify different lists of flavors.\n' + 'An entry with the path "assets/" does not specify any flavors.\n' + 'An entry with the path "assets/vanillaOrange.png" specifies the flavor(s): "vanilla", "orange".\n\n' + 'Consider organizing assets with different flavors into different directories.'), + ); + }); + + testWithoutContext('throws a tool exit when a flavored folder contains a flavorless asset', () async { + testFileSystem.file('.packages').createSync(); + testFileSystem.file(testFileSystem.path.join('vanilla', 'vanilla.png')).createSync(recursive: true); + testFileSystem.file(testFileSystem.path.join('vanilla', 'flavorless.png')).createSync(recursive: true); + + testFileSystem.file('pubspec.yaml') + ..createSync() + ..writeAsStringSync(r''' + name: example + flutter: + assets: + - path: vanilla/ + flavors: + - vanilla + - vanilla/flavorless.png + '''); + expect( + buildBundleWithFlavor(null), + throwsToolExit(message: 'Multiple assets entries include the file ' + '"vanilla/flavorless.png", but they specify different lists of flavors.\n' + 'An entry with the path "vanilla/" specifies the flavor(s): "vanilla".\n' + 'An entry with the path "vanilla/flavorless.png" does not specify any flavors.\n\n' + 'Consider organizing assets with different flavors into different directories.'), + ); + }); + + testWithoutContext('tool exits when two file-explicit entries give the same asset different flavors', () { + testFileSystem.file('.packages').createSync(); + testFileSystem.file('orange.png').createSync(recursive: true); + testFileSystem.file('pubspec.yaml') + ..createSync() + ..writeAsStringSync(r''' + name: example + flutter: + assets: + - path: orange.png + flavors: + - orange + - path: orange.png + flavors: + - mango + '''); + + expect( + buildBundleWithFlavor(null), + throwsToolExit(message: 'Multiple assets entries include the file ' + '"orange.png", but they specify different lists of flavors.\n' + 'An entry with the path "orange.png" specifies the flavor(s): "orange".\n' + 'An entry with the path "orange.png" specifies the flavor(s): "mango".'), + ); + }); + + testWithoutContext('throws ToolExit when flavor from file-level declaration has different flavor from containing folder flavor declaration', () async { + testFileSystem.file('.packages').createSync(); + testFileSystem.file(testFileSystem.path.join('vanilla', 'actually-strawberry.png')).createSync(recursive: true); + testFileSystem.file(testFileSystem.path.join('vanilla', 'vanilla.png')).createSync(recursive: true); + + testFileSystem.file('pubspec.yaml') + ..createSync() + ..writeAsStringSync(r''' + name: example + flutter: + assets: + - path: vanilla/ + flavors: + - vanilla + - path: vanilla/actually-strawberry.png + flavors: + - strawberry + '''); + expect( + buildBundleWithFlavor(null), + throwsToolExit(message: 'Multiple assets entries include the file ' + '"vanilla/actually-strawberry.png", but they specify different lists of flavors.\n' + 'An entry with the path "vanilla/" specifies the flavor(s): "vanilla".\n' + 'An entry with the path "vanilla/actually-strawberry.png" ' + 'specifies the flavor(s): "strawberry".'), + ); + }); + }); }); group('AssetBundle.build (web builds)', () { diff --git a/packages/flutter_tools/test/general.shard/base/deferred_component_test.dart b/packages/flutter_tools/test/general.shard/base/deferred_component_test.dart index 3a718f40f29..c2acb6829f1 100644 --- a/packages/flutter_tools/test/general.shard/base/deferred_component_test.dart +++ b/packages/flutter_tools/test/general.shard/base/deferred_component_test.dart @@ -6,6 +6,7 @@ import 'package:file/memory.dart'; import 'package:flutter_tools/src/base/deferred_component.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/flutter_manifest.dart'; import '../../src/common.dart'; @@ -15,18 +16,27 @@ void main() { final DeferredComponent component = DeferredComponent( name: 'bestcomponent', libraries: ['lib1', 'lib2'], - assets: [Uri.file('asset1'), Uri.file('asset2')], + assets: [ + AssetsEntry(uri: Uri.file('asset1')), + AssetsEntry(uri: Uri.file('asset2')), + ], ); expect(component.name, 'bestcomponent'); expect(component.libraries, ['lib1', 'lib2']); - expect(component.assets, [Uri.file('asset1'), Uri.file('asset2')]); + expect(component.assets, [ + AssetsEntry(uri: Uri.file('asset1')), + AssetsEntry(uri: Uri.file('asset2')), + ]); }); testWithoutContext('assignLoadingUnits selects the needed loading units and sets assigned', () { final DeferredComponent component = DeferredComponent( name: 'bestcomponent', libraries: ['lib1', 'lib2'], - assets: [Uri.file('asset1'), Uri.file('asset2')], + assets: [ + AssetsEntry(uri: Uri.file('asset1')), + AssetsEntry(uri: Uri.file('asset2')), + ], ); expect(component.libraries, ['lib1', 'lib2']); expect(component.assigned, false); @@ -94,7 +104,10 @@ void main() { final DeferredComponent component = DeferredComponent( name: 'bestcomponent', libraries: ['lib1', 'lib2'], - assets: [Uri.file('asset1'), Uri.file('asset2')], + assets: [ + AssetsEntry(uri: Uri.file('asset1')), + AssetsEntry(uri: Uri.file('asset2')), + ], ); expect(component.toString(), '\nDeferredComponent: bestcomponent\n Libraries:\n - lib1\n - lib2\n Assets:\n - asset1\n - asset2'); }); @@ -103,7 +116,10 @@ void main() { final DeferredComponent component = DeferredComponent( name: 'bestcomponent', libraries: ['lib1', 'lib2'], - assets: [Uri.file('asset1'), Uri.file('asset2')], + assets: [ + AssetsEntry(uri: Uri.file('asset1')), + AssetsEntry(uri: Uri.file('asset2')), + ], ); component.assignLoadingUnits([LoadingUnit(id: 2, libraries: ['lib1'])]); expect(component.toString(), '\nDeferredComponent: bestcomponent\n Libraries:\n - lib1\n - lib2\n LoadingUnits:\n - 2\n Assets:\n - asset1\n - asset2'); diff --git a/packages/flutter_tools/test/general.shard/build_info_test.dart b/packages/flutter_tools/test/general.shard/build_info_test.dart index ae1c92529da..0ef3c35945a 100644 --- a/packages/flutter_tools/test/general.shard/build_info_test.dart +++ b/packages/flutter_tools/test/general.shard/build_info_test.dart @@ -207,7 +207,7 @@ void main() { }); testWithoutContext('toEnvironmentConfig encoding of standard values', () { - const BuildInfo buildInfo = BuildInfo(BuildMode.debug, '', + const BuildInfo buildInfo = BuildInfo(BuildMode.debug, 'strawberry', treeShakeIcons: true, trackWidgetCreation: true, dartDefines: ['foo=2', 'bar=2'], @@ -220,7 +220,7 @@ void main() { packagesPath: 'foo/.dart_tool/package_config.json', codeSizeDirectory: 'foo/code-size', // These values are ignored by toEnvironmentConfig - androidProjectArgs: ['foo=bar', 'fizz=bazz'] + androidProjectArgs: ['foo=bar', 'fizz=bazz'], ); expect(buildInfo.toEnvironmentConfig(), { @@ -235,6 +235,7 @@ void main() { 'BUNDLE_SKSL_PATH': 'foo/bar/baz.sksl.json', 'PACKAGE_CONFIG': 'foo/.dart_tool/package_config.json', 'CODE_SIZE_DIRECTORY': 'foo/code-size', + 'FLAVOR': 'strawberry', }); }); diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/assets_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/assets_test.dart index e021361f038..0a06c7d6035 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/assets_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/assets_test.dart @@ -32,6 +32,7 @@ void main() { fileSystem: fileSystem, logger: BufferLogger.test(), platform: FakePlatform(), + defines: {}, ); fileSystem.file(environment.buildDir.childFile('app.dill')).createSync(recursive: true); fileSystem.file('packages/flutter_tools/lib/src/build_system/targets/assets.dart') @@ -93,6 +94,69 @@ flutter: ProcessManager: () => FakeProcessManager.any(), }); + group("Only copies assets with a flavor if the assets' flavor matches the flavor in the environment", () { + testUsingContext('When the environment does not have a flavor defined', () async { + fileSystem.file('pubspec.yaml') + ..createSync() + ..writeAsStringSync(''' + name: example + flutter: + assets: + - assets/common/ + - path: assets/vanilla/ + flavors: + - vanilla + - path: assets/strawberry/ + flavors: + - strawberry + '''); + + fileSystem.file('assets/common/image.png').createSync(recursive: true); + fileSystem.file('assets/vanilla/ice-cream.png').createSync(recursive: true); + fileSystem.file('assets/strawberry/ice-cream.png').createSync(recursive: true); + + await const CopyAssets().build(environment); + + expect(fileSystem.file('${environment.buildDir.path}/flutter_assets/assets/common/image.png'), exists); + expect(fileSystem.file('${environment.buildDir.path}/flutter_assets/assets/vanilla/ice-cream.png'), isNot(exists)); + expect(fileSystem.file('${environment.buildDir.path}/flutter_assets/assets/strawberry/ice-cream.png'), isNot(exists)); + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('When the environment has a flavor defined', () async { + environment.defines[kFlavor] = 'strawberry'; + fileSystem.file('pubspec.yaml') + ..createSync() + ..writeAsStringSync(''' + name: example + flutter: + assets: + - assets/common/ + - path: assets/vanilla/ + flavors: + - vanilla + - path: assets/strawberry/ + flavors: + - strawberry + '''); + + fileSystem.file('assets/common/image.png').createSync(recursive: true); + fileSystem.file('assets/vanilla/ice-cream.png').createSync(recursive: true); + fileSystem.file('assets/strawberry/ice-cream.png').createSync(recursive: true); + + await const CopyAssets().build(environment); + + expect(fileSystem.file('${environment.buildDir.path}/flutter_assets/assets/common/image.png'), exists); + expect(fileSystem.file('${environment.buildDir.path}/flutter_assets/assets/vanilla/ice-cream.png'), isNot(exists)); + expect(fileSystem.file('${environment.buildDir.path}/flutter_assets/assets/strawberry/ice-cream.png'), exists); + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.any(), + }); + }); + testUsingContext('Throws exception if pubspec contains missing files', () async { fileSystem.file('pubspec.yaml') ..createSync() diff --git a/packages/flutter_tools/test/general.shard/devfs_test.dart b/packages/flutter_tools/test/general.shard/devfs_test.dart index d4b4e5fcf10..4872cddaa6c 100644 --- a/packages/flutter_tools/test/general.shard/devfs_test.dart +++ b/packages/flutter_tools/test/general.shard/devfs_test.dart @@ -733,7 +733,14 @@ class FakeBundle extends AssetBundle { List get additionalDependencies => []; @override - Future build({String manifestPath = defaultManifestPath, String? assetDirPath, String? packagesPath, bool deferredComponentsEnabled = false, TargetPlatform? targetPlatform}) async { + Future build({ + String manifestPath = defaultManifestPath, + String? assetDirPath, + String? packagesPath, + bool deferredComponentsEnabled = false, + TargetPlatform? targetPlatform, + String? flavor, + }) async { return 0; } diff --git a/packages/flutter_tools/test/general.shard/flutter_manifest_test.dart b/packages/flutter_tools/test/general.shard/flutter_manifest_test.dart index d0ca23fc3bf..dc048f5c8fe 100644 --- a/packages/flutter_tools/test/general.shard/flutter_manifest_test.dart +++ b/packages/flutter_tools/test/general.shard/flutter_manifest_test.dart @@ -13,12 +13,17 @@ import 'package:flutter_tools/src/flutter_manifest.dart'; import '../src/common.dart'; void main() { + late BufferLogger logger; + setUpAll(() { Cache.flutterRoot = getFlutterRoot(); }); + setUp(() { + logger = BufferLogger.test(); + }); + testWithoutContext('FlutterManifest is empty when the pubspec.yaml file is empty', () async { - final BufferLogger logger = BufferLogger.test(); final FlutterManifest flutterManifest = FlutterManifest.createFromString( '', logger: logger, @@ -34,7 +39,6 @@ void main() { }); testWithoutContext('FlutterManifest is null when the pubspec.yaml file is not a map', () async { - final BufferLogger logger = BufferLogger.test(); expect(FlutterManifest.createFromString( 'Not a map', logger: logger, @@ -50,7 +54,6 @@ dependencies: flutter: sdk: flutter '''; - final BufferLogger logger = BufferLogger.test(); final FlutterManifest flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -74,7 +77,6 @@ dependencies: flutter: uses-material-design: true '''; - final BufferLogger logger = BufferLogger.test(); final FlutterManifest flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -92,7 +94,6 @@ dependencies: flutter: generate: true '''; - final BufferLogger logger = BufferLogger.test(); final FlutterManifest flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -110,7 +111,6 @@ dependencies: flutter: generate: "invalid" '''; - final BufferLogger logger = BufferLogger.test(); final FlutterManifest flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -128,7 +128,6 @@ dependencies: flutter: generate: false '''; - final BufferLogger logger = BufferLogger.test(); final FlutterManifest flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -149,18 +148,38 @@ flutter: - a/foo - a/bar '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, )!; - expect(flutterManifest.assets, [ - Uri.parse('a/foo'), - Uri.parse('a/bar'), + expect(flutterManifest.assets, [ + AssetsEntry(uri: Uri.parse('a/foo')), + AssetsEntry(uri: Uri.parse('a/bar')), ]); }); + testWithoutContext('FlutterManifest assets entry flavor is not a string', () async { + const String manifest = ''' +name: test +dependencies: + flutter: + sdk: flutter +flutter: + uses-material-design: true + assets: + - assets/folder/ + - path: assets/vanilla/ + flavors: + - key1: value1 + key2: value2 +'''; + FlutterManifest.createFromString(manifest, logger: logger); + expect(logger.errorText, contains('Asset manifest entry is malformed. ' + 'Expected "flavors" entry to be a list of strings.')); + }); + testWithoutContext('FlutterManifest has one font family with one asset', () async { const String manifest = ''' name: test @@ -174,7 +193,7 @@ flutter: fonts: - asset: a/bar '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -211,7 +230,7 @@ flutter: - asset: a/bar weight: 400 '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -251,7 +270,7 @@ flutter: weight: 400 style: italic '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -297,7 +316,7 @@ flutter: asset: a/baz style: italic '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -359,7 +378,7 @@ flutter: asset: a/baz style: italic '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -405,7 +424,7 @@ flutter: weight: 400 style: italic '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -448,7 +467,7 @@ flutter: style: italic - family: bar '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -487,7 +506,7 @@ flutter: fonts: - weight: 400 '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -505,7 +524,7 @@ dependencies: sdk: flutter flutter: '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -526,7 +545,7 @@ flutter: androidPackage: com.example androidX: true '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -544,7 +563,7 @@ flutter: plugin: androidPackage: com.example '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -565,7 +584,7 @@ flutter: package: com.example pluginClass: TestPlugin '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -585,7 +604,7 @@ flutter: ios: pluginClass: HelloPlugin '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -601,7 +620,7 @@ name: test flutter: plugin: '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest? flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -621,7 +640,7 @@ dependencies: sdk: flutter flutter: '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest? flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -643,7 +662,7 @@ dependencies: sdk: flutter flutter: '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest? flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -664,7 +683,7 @@ dependencies: sdk: flutter flutter: '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest? flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -686,7 +705,7 @@ dependencies: sdk: flutter flutter: '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest? flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -708,7 +727,7 @@ dependencies: sdk: flutter flutter: '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest? flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -725,7 +744,7 @@ dependencies: sdk: flutter flutter: '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest? flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -747,7 +766,7 @@ flutter: fonts: -asset: a/bar '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest? flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -767,7 +786,7 @@ dependencies: flutter: fonts: [] '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest? flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -786,7 +805,7 @@ dependencies: flutter: assets: [] '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest? flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -809,7 +828,7 @@ flutter: fonts: - asset '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest? flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -833,7 +852,7 @@ flutter: fonts: -asset: a/bar '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest? flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -858,7 +877,7 @@ flutter: - asset: a/bar - string '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest? flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -880,15 +899,13 @@ flutter: - lib/gallery/example_code.dart - '''; - final BufferLogger logger = BufferLogger.test(); - final FlutterManifest flutterManifest = FlutterManifest.createFromString( + + FlutterManifest.createFromString( manifest, logger: logger, - )!; - final List assets = flutterManifest.assets; + ); expect(logger.errorText, contains('Asset manifest contains a null or empty uri.')); - expect(assets, hasLength(1)); }); testWithoutContext('FlutterManifest handles special characters in asset URIs', () { @@ -904,18 +921,18 @@ flutter: - lib/gallery/abc?xyz - lib/gallery/aaa bbb '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, )!; - final List assets = flutterManifest.assets; + final List assets = flutterManifest.assets; expect(assets, hasLength(3)); - expect(assets, [ - Uri.parse('lib/gallery/abc%23xyz'), - Uri.parse('lib/gallery/abc%3Fxyz'), - Uri.parse('lib/gallery/aaa%20bbb'), + expect(assets, [ + AssetsEntry(uri: Uri.parse('lib/gallery/abc%23xyz')), + AssetsEntry(uri: Uri.parse('lib/gallery/abc%3Fxyz')), + AssetsEntry(uri: Uri.parse('lib/gallery/aaa%20bbb')), ]); }); @@ -929,7 +946,7 @@ dependencies: flutter: - uses-material-design: true '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest? flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -954,7 +971,7 @@ flutter: '''; final FileSystem fileSystem = MemoryFileSystem.test(); fileSystem.file('pubspec.yaml').writeAsStringSync(manifest); - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest flutterManifest = FlutterManifest.createFromPath( 'pubspec.yaml', fileSystem: fileSystem, @@ -975,7 +992,7 @@ flutter: final FileSystem fileSystem = MemoryFileSystem.test(style: FileSystemStyle.windows); fileSystem.file('pubspec.yaml').writeAsStringSync(manifest); - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest flutterManifest = FlutterManifest.createFromPath( 'pubspec.yaml', fileSystem: fileSystem, @@ -992,7 +1009,7 @@ flutter: plugin: androidPackage: com.example '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -1011,7 +1028,7 @@ flutter: some_platform: pluginClass: SomeClass '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -1032,7 +1049,7 @@ flutter: ios: pluginClass: SomeClass '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -1058,7 +1075,7 @@ flutter: ios: pluginClass: SomeClass '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -1082,7 +1099,7 @@ flutter: platforms: - android '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest? flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -1104,7 +1121,7 @@ flutter: ios: pluginClass: SomeClass '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest? flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -1124,7 +1141,7 @@ dependencies: flutter: licenses: [] '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest? flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -1144,7 +1161,7 @@ flutter: licenses: - foo.txt '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -1162,7 +1179,7 @@ dependencies: flutter: licenses: foo.txt '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest? flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -1183,7 +1200,7 @@ flutter: - foo.txt - bar: fizz '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest? flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -1207,7 +1224,7 @@ flutter: assets: - path/to/asset.jpg '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -1220,7 +1237,7 @@ flutter: expect(deferredComponents[0].libraries.length, 1); expect(deferredComponents[0].libraries[0], 'lib1'); expect(deferredComponents[0].assets.length, 1); - expect(deferredComponents[0].assets[0].path, 'path/to/asset.jpg'); + expect(deferredComponents[0].assets[0].uri.path, 'path/to/asset.jpg'); }); testWithoutContext('FlutterManifest parses multiple deferred components', () async { @@ -1243,7 +1260,7 @@ flutter: assets: - path/to/asset2.jpg '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -1256,14 +1273,14 @@ flutter: expect(deferredComponents[0].libraries.length, 1); expect(deferredComponents[0].libraries[0], 'lib1'); expect(deferredComponents[0].assets.length, 1); - expect(deferredComponents[0].assets[0].path, 'path/to/asset.jpg'); + expect(deferredComponents[0].assets[0].uri.path, 'path/to/asset.jpg'); expect(deferredComponents[1].name, 'component2'); expect(deferredComponents[1].libraries.length, 2); expect(deferredComponents[1].libraries[0], 'lib2'); expect(deferredComponents[1].libraries[1], 'lib3'); expect(deferredComponents[1].assets.length, 1); - expect(deferredComponents[1].assets[0].path, 'path/to/asset2.jpg'); + expect(deferredComponents[1].assets[0].uri.path, 'path/to/asset2.jpg'); }); testWithoutContext('FlutterManifest parses empty deferred components', () async { @@ -1275,7 +1292,7 @@ dependencies: flutter: deferred-components: '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -1296,7 +1313,7 @@ flutter: - libraries: - lib1 '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest? flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -1315,7 +1332,7 @@ dependencies: flutter: deferred-components: blah '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest? flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -1336,7 +1353,7 @@ flutter: - name: blah libraries: blah '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest? flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -1358,7 +1375,7 @@ flutter: libraries: - not-a-string: '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest? flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -1380,14 +1397,14 @@ flutter: assets: - not-a-string: '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest? flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, ); expect(flutterManifest, null); - expect(logger.errorText, 'Expected "assets" key in the 0 element of "deferred-components" to be a list of file paths, but element 0 was a YamlMap\n'); + expect(logger.errorText, 'Asset manifest entry is malformed. Expected asset entry to be either a string or a map containing a "path" entry. Got Null instead.\n'); }); testWithoutContext('FlutterManifest deferred component multiple assets is string', () async { @@ -1404,14 +1421,14 @@ flutter: - also-not-a-string: - woo '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest? flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, ); expect(flutterManifest, null); - expect(logger.errorText, 'Expected "assets" key in the 0 element of "deferred-components" to be a list of file paths, but element 1 was a YamlMap\n'); + expect(logger.errorText, 'Asset manifest entry is malformed. Expected asset entry to be either a string or a map containing a "path" entry. Got Null instead.\n'); }); testWithoutContext('FlutterManifest multiple deferred components assets is string', () async { @@ -1431,14 +1448,14 @@ flutter: - not-a-string: - woo '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest? flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, ); expect(flutterManifest, null); - expect(logger.errorText, 'Expected "assets" key in the 1 element of "deferred-components" to be a list of file paths, but element 1 was a YamlMap\n'); + expect(logger.errorText, 'Asset manifest entry is malformed. Expected asset entry to be either a string or a map containing a "path" entry. Got Null instead.\n'); }); testWithoutContext('FlutterManifest deferred component assets is list', () async { @@ -1452,7 +1469,7 @@ flutter: - name: blah assets: blah '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest? flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -1476,7 +1493,7 @@ flutter: - path/to/asset2.jpg - path/to/asset3.jpg '''; - final BufferLogger logger = BufferLogger.test(); + final FlutterManifest flutterManifest = FlutterManifest.createFromString( manifest, logger: logger, @@ -1488,9 +1505,9 @@ flutter: expect(deferredComponents[0].name, 'component1'); expect(deferredComponents[0].libraries.length, 0); expect(deferredComponents[0].assets.length, 3); - expect(deferredComponents[0].assets[0].path, 'path/to/asset1.jpg'); - expect(deferredComponents[0].assets[1].path, 'path/to/asset2.jpg'); - expect(deferredComponents[0].assets[2].path, 'path/to/asset3.jpg'); + expect(deferredComponents[0].assets[0].uri.path, 'path/to/asset1.jpg'); + expect(deferredComponents[0].assets[1].uri.path, 'path/to/asset2.jpg'); + expect(deferredComponents[0].assets[2].uri.path, 'path/to/asset3.jpg'); }); testWithoutContext('FlutterManifest can parse empty dependencies', () async { diff --git a/packages/flutter_tools/test/general.shard/xcode_backend_test.dart b/packages/flutter_tools/test/general.shard/xcode_backend_test.dart index 52156444376..b91968bd9fd 100644 --- a/packages/flutter_tools/test/general.shard/xcode_backend_test.dart +++ b/packages/flutter_tools/test/general.shard/xcode_backend_test.dart @@ -160,6 +160,7 @@ void main() { 'FRONTEND_SERVER_STARTER_PATH': frontendServerStarterPath, 'INFOPLIST_PATH': 'Info.plist', 'SDKROOT': sdkRoot, + 'FLAVOR': 'strawberry', 'SPLIT_DEBUG_INFO': splitDebugInfo, 'TRACK_WIDGET_CREATION': trackWidgetCreation, 'TREE_SHAKE_ICONS': treeShake, @@ -174,6 +175,7 @@ void main() { '-dTargetPlatform=ios', '-dTargetFile=lib/main.dart', '-dBuildMode=${buildMode.toLowerCase()}', + '-dFlavor=strawberry', '-dIosArchs=$archs', '-dSdkRoot=$sdkRoot', '-dSplitDebugInfo=$splitDebugInfo',