[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
This commit is contained in:
Andrew Kolos 2023-12-13 21:30:10 -08:00 committed by GitHub
parent f5050cf4a4
commit 935775cb74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 880 additions and 212 deletions

View File

@ -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

View File

@ -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<void> main() async {
deviceOperatingSystem = DeviceOperatingSystem.android;
@ -15,31 +20,20 @@ Future<void> 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: <String>['--debug', '--flavor', 'paid'],
);
await flutter(
'install',
options: <String>['--debug', '--flavor', 'paid', '--uninstall-only'],
);
final List<TaskResult> testResults = <TaskResult>[
await _testInstallDebugPaidFlavor(projectPath),
await _testInstallBogusFlavor(),
];
final StringBuffer stderr = StringBuffer();
await evalFlutter(
'install',
canFail: true,
stderr: stderr,
options: <String>['--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<void> main() async {
return installTestsResult;
});
}
// Ensures installation works. Also tests asset bundling while we are at it.
Future<TaskResult> _testInstallDebugPaidFlavor(String projectDir) async {
await evalFlutter(
'install',
options: <String>['--debug', '--flavor', 'paid'],
);
final Uint8List assetManifestFileData = File(
path.join(projectDir, 'build', 'app', 'intermediates', 'assets', 'paidDebug', 'flutter_assets', 'AssetManifest.bin'),
).readAsBytesSync();
final Map<Object?, Object?> assetManifest = const StandardMessageCodec()
.decodeMessage(ByteData.sublistView(assetManifestFileData)) as Map<Object?, Object?>;
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: <String>['--debug', '--flavor', 'paid', '--uninstall-only'],
);
return TaskResult.success(null);
}
Future<TaskResult> _testInstallBogusFlavor() async {
final StringBuffer stderr = StringBuffer();
await evalFlutter(
'install',
canFail: true,
stderr: stderr,
options: <String>['--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);
}

View File

@ -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<void> main() async {
deviceOperatingSystem = DeviceOperatingSystem.ios;
@ -14,29 +20,20 @@ Future<void> 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: <String>['--flavor', 'paid'],
);
await flutter(
'install',
options: <String>['--flavor', 'paid', '--uninstall-only'],
);
final StringBuffer stderr = StringBuffer();
await evalFlutter(
'install',
canFail: true,
stderr: stderr,
options: <String>['--flavor', 'bogus'],
);
final List<TaskResult> testResults = <TaskResult>[
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<void> main() async {
return installTestsResult;
});
}
Future<TaskResult> _testInstallDebugPaidFlavor(String projectDir) async {
await evalFlutter(
'install',
options: <String>['--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<Object?, Object?> assetManifest = const StandardMessageCodec()
.decodeMessage(ByteData.sublistView(assetManifestFileData)) as Map<Object?, Object?>;
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: <String>['--flavor', 'paid', '--uninstall-only'],
);
return TaskResult.success(null);
}
Future<TaskResult> _testInstallBogusFlavor() async {
final StringBuffer stderr = StringBuffer();
await evalFlutter(
'install',
canFail: true,
stderr: stderr,
options: <String>['--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);
}

View File

@ -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

View File

@ -0,0 +1 @@
this is a test asset not meant for any specific flavor

View File

@ -0,0 +1 @@
this is a test asset for --flavor free

View File

@ -0,0 +1 @@
this is a test asset for --flavor paid

View File

@ -0,0 +1 @@
premium

View File

@ -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

View File

@ -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'] ?? ''}',

View File

@ -1057,6 +1057,7 @@ class FlutterPlugin implements Plugin<Project> {
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<Project> {
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}"
}

View File

@ -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<Uri> wildcardDirectories,
Directory projectDirectory, {
List<String> excludeDirs = const <String>[],
String? flavor,
}) {
final List<DeferredComponent>? components = flutterManifest.deferredComponents;
final Map<String, Map<_Asset, List<_Asset>>> deferredComponentsAssetVariants = <String, Map<_Asset, List<_Asset>>>{};
@ -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<String>? 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<String> excludeDirs = const <String>[],
Uri? originUri,
String? packageName,
Package? attributedPackage,
AssetKind assetKind = AssetKind.regular,
List<String>? 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<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 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<String>? 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: <String>['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<String>? 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<String>? flavors,
}): originUri = originUri ?? entryUri, flavors = flavors ?? const <String>[];
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<String> 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<String> assetFlavors = flavors.toSet();
final Set<String> 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(<Object>[
baseDir,
relativeUri,
entryUri,
assetKind,
...flavors,
]);
}
// Given an assets directory like this:

View File

@ -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 <String>[],
this.assets = const <Uri>[],
this.assets = const <AssetsEntry>[],
}) : _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<String> libraries;
/// Assets that are part of this component as a Uri relative to the project directory.
final List<Uri> assets;
/// Assets that are part of this component.
final List<AssetsEntry> 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();
}

View File

@ -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';

View File

@ -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,

View File

@ -34,6 +34,7 @@ Future<Depfile> copyAssets(
BuildMode? buildMode,
required ShaderTarget shaderTarget,
List<File> additionalInputs = const <File>[],
String? flavor,
}) async {
// Check for an SkSL bundle.
final String? shaderBundlePath = environment.defines[kBundleSkSLPath] ?? environment.inputs[kBundleSkSLPath];
@ -58,6 +59,7 @@ Future<Depfile> 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,

View File

@ -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,

View File

@ -533,6 +533,7 @@ abstract class IosAssetBundle extends Target {
flutterProject.ios.infoPlist,
flutterProject.ios.appFrameworkInfoPlist,
],
flavor: environment.defines[kFlavor],
);
environment.depFileService.writeToFile(
assetDepfile,

View File

@ -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,

View File

@ -114,6 +114,7 @@ Future<AssetBundle?> buildAssets({
String? assetDirPath,
String? packagesPath,
TargetPlatform? targetPlatform,
String? flavor,
}) async {
assetDirPath ??= getAssetBuildDirectory();
packagesPath ??= globals.fs.path.absolute('.packages');
@ -124,6 +125,7 @@ Future<AssetBundle?> buildAssets({
manifestPath: manifestPath,
packagesPath: packagesPath,
targetPlatform: targetPlatform,
flavor: flavor,
);
if (result != 0) {
return null;

View File

@ -231,29 +231,12 @@ class FlutterManifest {
_logger.printError('Expected deferred component manifest to be a map.');
continue;
}
List<Uri> assetsUri = <Uri>[];
final List<Object?>? assets = component['assets'] as List<Object?>?;
if (assets == null) {
assetsUri = const <Uri>[];
} 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 ?
<String>[] : (component['libraries'] as List<dynamic>).cast<String>(),
assets: assetsUri,
assets: _computeAssets(component['assets']),
)
);
}
@ -311,26 +294,7 @@ class FlutterManifest {
: fontList.map<Map<String, Object?>?>(castStringKeyedMap).whereType<Map<String, Object?>>().toList();
}
late final List<Uri> assets = _computeAssets();
List<Uri> _computeAssets() {
final List<Object?>? assets = _flutterDescriptor['assets'] as List<Object?>?;
if (assets == null) {
return const <Uri>[];
}
final List<Uri> results = <Uri>[];
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<AssetsEntry> assets = _computeAssets(_flutterDescriptor['assets']);
late final List<Font> fonts = _extractFonts();
@ -521,15 +485,7 @@ void _validateFlutter(YamlMap? yaml, List<String> 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<Object?, Object?> kvp, List<String> 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<String>(assets, errors, '"assets" key in the $i element of "${kvp.key}"', 'file paths');
}
errors.addAll(_validateAssets(valueMap['assets']));
}
}
}
}
List<String> _validateAssets(Object? yaml) {
final (_, List<String> 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<AssetsEntry>, List<String> errors) _computeAssetsSafe(Object? yaml) {
if (yaml == null) {
return (const <AssetsEntry>[], const <String>[]);
}
if (yaml is! YamlList) {
final String error = 'Expected "assets" to be a list, but got $yaml (${yaml.runtimeType}).';
return (const <AssetsEntry>[], <String>[error]);
}
final List<AssetsEntry> results = <AssetsEntry>[];
final List<String> errors = <String>[];
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<AssetsEntry> _computeAssets(Object? assetsSection) {
final (List<AssetsEntry> result, List<String> errors) = _computeAssetsSafe(assetsSection);
if (errors.isNotEmpty) {
throw Exception('Uncaught error(s) in assets section: '
'${errors.join('\n')}');
}
return result;
}
void _validateFonts(YamlList fonts, List<String> errors) {
const Set<int> fontWeights = <int>{
100, 200, 300, 400, 500, 600, 700, 800, 900,
@ -703,3 +694,103 @@ void _validateFonts(YamlList fonts, List<String> errors) {
}
}
}
/// Represents an entry under the `assets` section of a pubspec.
@immutable
class AssetsEntry {
const AssetsEntry({
required this.uri,
this.flavors = const <String>[],
});
final Uri uri;
final List<String> 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<String> flavorsListErrors = <String>[];
_validateListType<String>(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<String>.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);
}

View File

@ -141,6 +141,8 @@ class HotRunner extends ResidentRunner {
NativeAssetsBuildRunner? _buildRunner;
String? flavor;
Future<void> _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();
}

View File

@ -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;

View File

@ -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<ManifestAssetBundle> 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)', () {

View File

@ -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: <String>['lib1', 'lib2'],
assets: <Uri>[Uri.file('asset1'), Uri.file('asset2')],
assets: <AssetsEntry>[
AssetsEntry(uri: Uri.file('asset1')),
AssetsEntry(uri: Uri.file('asset2')),
],
);
expect(component.name, 'bestcomponent');
expect(component.libraries, <String>['lib1', 'lib2']);
expect(component.assets, <Uri>[Uri.file('asset1'), Uri.file('asset2')]);
expect(component.assets, <AssetsEntry>[
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: <String>['lib1', 'lib2'],
assets: <Uri>[Uri.file('asset1'), Uri.file('asset2')],
assets: <AssetsEntry>[
AssetsEntry(uri: Uri.file('asset1')),
AssetsEntry(uri: Uri.file('asset2')),
],
);
expect(component.libraries, <String>['lib1', 'lib2']);
expect(component.assigned, false);
@ -94,7 +104,10 @@ void main() {
final DeferredComponent component = DeferredComponent(
name: 'bestcomponent',
libraries: <String>['lib1', 'lib2'],
assets: <Uri>[Uri.file('asset1'), Uri.file('asset2')],
assets: <AssetsEntry>[
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: <String>['lib1', 'lib2'],
assets: <Uri>[Uri.file('asset1'), Uri.file('asset2')],
assets: <AssetsEntry>[
AssetsEntry(uri: Uri.file('asset1')),
AssetsEntry(uri: Uri.file('asset2')),
],
);
component.assignLoadingUnits(<LoadingUnit>[LoadingUnit(id: 2, libraries: <String>['lib1'])]);
expect(component.toString(), '\nDeferredComponent: bestcomponent\n Libraries:\n - lib1\n - lib2\n LoadingUnits:\n - 2\n Assets:\n - asset1\n - asset2');

View File

@ -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: <String>['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: <String>['foo=bar', 'fizz=bazz']
androidProjectArgs: <String>['foo=bar', 'fizz=bazz'],
);
expect(buildInfo.toEnvironmentConfig(), <String, String>{
@ -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',
});
});

View File

@ -32,6 +32,7 @@ void main() {
fileSystem: fileSystem,
logger: BufferLogger.test(),
platform: FakePlatform(),
defines: <String, String>{},
);
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: <Type, Generator>{
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: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
});
testUsingContext('Throws exception if pubspec contains missing files', () async {
fileSystem.file('pubspec.yaml')
..createSync()

View File

@ -733,7 +733,14 @@ class FakeBundle extends AssetBundle {
List<File> get additionalDependencies => <File>[];
@override
Future<int> build({String manifestPath = defaultManifestPath, String? assetDirPath, String? packagesPath, bool deferredComponentsEnabled = false, TargetPlatform? targetPlatform}) async {
Future<int> build({
String manifestPath = defaultManifestPath,
String? assetDirPath,
String? packagesPath,
bool deferredComponentsEnabled = false,
TargetPlatform? targetPlatform,
String? flavor,
}) async {
return 0;
}

View File

@ -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>[
Uri.parse('a/foo'),
Uri.parse('a/bar'),
expect(flutterManifest.assets, <AssetsEntry>[
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<Uri> 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<Uri> assets = flutterManifest.assets;
final List<AssetsEntry> assets = flutterManifest.assets;
expect(assets, hasLength(3));
expect(assets, <Uri>[
Uri.parse('lib/gallery/abc%23xyz'),
Uri.parse('lib/gallery/abc%3Fxyz'),
Uri.parse('lib/gallery/aaa%20bbb'),
expect(assets, <AssetsEntry>[
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 {

View File

@ -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',