Unverified Commit 935775cb authored by Andrew Kolos's avatar Andrew Kolos Committed by GitHub

[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
parent f5050cf4
...@@ -58,6 +58,7 @@ dependencies: ...@@ -58,6 +58,7 @@ dependencies:
source_maps: 0.10.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" 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" 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" 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" 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" 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" term_glyph: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
...@@ -74,4 +75,4 @@ dependencies: ...@@ -74,4 +75,4 @@ dependencies:
dev_dependencies: dev_dependencies:
test_api: 0.6.1 test_api: 0.6.1
# PUBSPEC CHECKSUM: 29d2 # PUBSPEC CHECKSUM: b875
...@@ -2,12 +2,17 @@ ...@@ -2,12 +2,17 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // 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/devices.dart';
import 'package:flutter_devicelab/framework/framework.dart'; import 'package:flutter_devicelab/framework/framework.dart';
import 'package:flutter_devicelab/framework/task_result.dart'; import 'package:flutter_devicelab/framework/task_result.dart';
import 'package:flutter_devicelab/framework/utils.dart'; import 'package:flutter_devicelab/framework/utils.dart';
import 'package:flutter_devicelab/tasks/integration_tests.dart'; import 'package:flutter_devicelab/tasks/integration_tests.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:standard_message_codec/standard_message_codec.dart';
Future<void> main() async { Future<void> main() async {
deviceOperatingSystem = DeviceOperatingSystem.android; deviceOperatingSystem = DeviceOperatingSystem.android;
...@@ -15,31 +20,20 @@ Future<void> main() async { ...@@ -15,31 +20,20 @@ Future<void> main() async {
await createFlavorsTest().call(); await createFlavorsTest().call();
await createIntegrationTestFlavorsTest().call(); await createIntegrationTestFlavorsTest().call();
final String projectPath = '${flutterDirectory.path}/dev/integration_tests/flavors';
final TaskResult installTestsResult = await inDirectory( final TaskResult installTestsResult = await inDirectory(
'${flutterDirectory.path}/dev/integration_tests/flavors', projectPath,
() async { () async {
await flutter( final List<TaskResult> testResults = <TaskResult>[
'install', await _testInstallDebugPaidFlavor(projectPath),
options: <String>['--debug', '--flavor', 'paid'], await _testInstallBogusFlavor(),
); ];
await flutter(
'install', final TaskResult? firstInstallFailure = testResults
options: <String>['--debug', '--flavor', 'paid', '--uninstall-only'], .firstWhereOrNull((TaskResult element) => element.failed);
);
if (firstInstallFailure != null) {
final StringBuffer stderr = StringBuffer(); return firstInstallFailure;
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); return TaskResult.success(null);
...@@ -49,3 +43,49 @@ Future<void> main() async { ...@@ -49,3 +43,49 @@ Future<void> main() async {
return installTestsResult; 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);
}
...@@ -2,11 +2,17 @@ ...@@ -2,11 +2,17 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // 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/devices.dart';
import 'package:flutter_devicelab/framework/framework.dart'; import 'package:flutter_devicelab/framework/framework.dart';
import 'package:flutter_devicelab/framework/task_result.dart'; import 'package:flutter_devicelab/framework/task_result.dart';
import 'package:flutter_devicelab/framework/utils.dart'; import 'package:flutter_devicelab/framework/utils.dart';
import 'package:flutter_devicelab/tasks/integration_tests.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 { Future<void> main() async {
deviceOperatingSystem = DeviceOperatingSystem.ios; deviceOperatingSystem = DeviceOperatingSystem.ios;
...@@ -14,29 +20,20 @@ Future<void> main() async { ...@@ -14,29 +20,20 @@ Future<void> main() async {
await createFlavorsTest().call(); await createFlavorsTest().call();
await createIntegrationTestFlavorsTest().call(); await createIntegrationTestFlavorsTest().call();
// test install and uninstall of flavors app // test install and uninstall of flavors app
final String projectDir = '${flutterDirectory.path}/dev/integration_tests/flavors';
final TaskResult installTestsResult = await inDirectory( final TaskResult installTestsResult = await inDirectory(
'${flutterDirectory.path}/dev/integration_tests/flavors', projectDir,
() async { () async {
await flutter( final List<TaskResult> testResults = <TaskResult>[
'install', await _testInstallDebugPaidFlavor(projectDir),
options: <String>['--flavor', 'paid'], await _testInstallBogusFlavor(),
); ];
await flutter(
'install', final TaskResult? firstInstallFailure = testResults
options: <String>['--flavor', 'paid', '--uninstall-only'], .firstWhereOrNull((TaskResult element) => element.failed);
);
final StringBuffer stderr = StringBuffer(); if (firstInstallFailure != null) {
await evalFlutter( return firstInstallFailure;
'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); return TaskResult.success(null);
...@@ -46,3 +43,56 @@ Future<void> main() async { ...@@ -46,3 +43,56 @@ Future<void> main() async {
return installTestsResult; 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);
}
...@@ -24,6 +24,7 @@ dependencies: ...@@ -24,6 +24,7 @@ dependencies:
web: 0.4.0 web: 0.4.0
webkit_inspection_protocol: 1.2.1 webkit_inspection_protocol: 1.2.1
xml: 6.5.0 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" _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" async: 2.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
...@@ -72,4 +73,4 @@ dev_dependencies: ...@@ -72,4 +73,4 @@ dev_dependencies:
watcher: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" 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" web_socket_channel: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
# PUBSPEC CHECKSUM: 6040 # PUBSPEC CHECKSUM: 70e2
this is a test asset not meant for any specific flavor
\ No newline at end of file
this is a test asset for --flavor free
\ No newline at end of file
this is a test asset for --flavor paid
\ No newline at end of file
...@@ -75,5 +75,13 @@ dev_dependencies: ...@@ -75,5 +75,13 @@ dev_dependencies:
flutter: flutter:
uses-material-design: true uses-material-design: true
assets:
- assets/common/common.txt
- path: assets/paid/
flavors:
- paid
- path: assets/free/
flavors:
- free
# PUBSPEC CHECKSUM: 6bd3 # PUBSPEC CHECKSUM: 6bd3
...@@ -401,6 +401,7 @@ class Context { ...@@ -401,6 +401,7 @@ class Context {
'-dTargetPlatform=ios', '-dTargetPlatform=ios',
'-dTargetFile=$targetPath', '-dTargetFile=$targetPath',
'-dBuildMode=$buildMode', '-dBuildMode=$buildMode',
if (environment['FLAVOR'] != null) '-dFlavor=${environment['FLAVOR']}',
'-dIosArchs=${environment['ARCHS'] ?? ''}', '-dIosArchs=${environment['ARCHS'] ?? ''}',
'-dSdkRoot=${environment['SDKROOT'] ?? ''}', '-dSdkRoot=${environment['SDKROOT'] ?? ''}',
'-dSplitDebugInfo=${environment['SPLIT_DEBUG_INFO'] ?? ''}', '-dSplitDebugInfo=${environment['SPLIT_DEBUG_INFO'] ?? ''}',
......
...@@ -1057,6 +1057,7 @@ class FlutterPlugin implements Plugin<Project> { ...@@ -1057,6 +1057,7 @@ class FlutterPlugin implements Plugin<Project> {
boolean isAndroidLibraryValue = isBuildingAar || isUsedAsSubproject boolean isAndroidLibraryValue = isBuildingAar || isUsedAsSubproject
String variantBuildMode = buildModeFor(variant.buildType) String variantBuildMode = buildModeFor(variant.buildType)
String flavorValue = variant.getFlavorName()
String taskName = toCamelCase(["compile", FLUTTER_BUILD_PREFIX, variant.name]) String taskName = toCamelCase(["compile", FLUTTER_BUILD_PREFIX, variant.name])
// Be careful when configuring task below, Groovy has bizarre // Be careful when configuring task below, Groovy has bizarre
// scoping rules: writing `verbose isVerbose()` means calling // scoping rules: writing `verbose isVerbose()` means calling
...@@ -1094,6 +1095,7 @@ class FlutterPlugin implements Plugin<Project> { ...@@ -1094,6 +1095,7 @@ class FlutterPlugin implements Plugin<Project> {
deferredComponents deferredComponentsValue deferredComponents deferredComponentsValue
validateDeferredComponents validateDeferredComponentsValue validateDeferredComponents validateDeferredComponentsValue
isAndroidLibrary isAndroidLibraryValue isAndroidLibrary isAndroidLibraryValue
flavor flavorValue
} }
File libJar = project.file("${project.buildDir}/$INTERMEDIATES_DIR/flutter/${variant.name}/libs.jar") 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) { Task packFlutterAppAotTask = project.tasks.create(name: "packLibs${FLUTTER_BUILD_PREFIX}${variant.name.capitalize()}", type: Jar) {
...@@ -1380,6 +1382,8 @@ abstract class BaseFlutterTask extends DefaultTask { ...@@ -1380,6 +1382,8 @@ abstract class BaseFlutterTask extends DefaultTask {
Boolean validateDeferredComponents Boolean validateDeferredComponents
@Optional @Input @Optional @Input
Boolean isAndroidLibrary Boolean isAndroidLibrary
@Optional @Input
String flavor
@OutputFiles @OutputFiles
FileCollection getDependenciesFiles() { FileCollection getDependenciesFiles() {
...@@ -1460,6 +1464,9 @@ abstract class BaseFlutterTask extends DefaultTask { ...@@ -1460,6 +1464,9 @@ abstract class BaseFlutterTask extends DefaultTask {
if (codeSizeDirectory != null) { if (codeSizeDirectory != null) {
args "-dCodeSizeDirectory=${codeSizeDirectory}" args "-dCodeSizeDirectory=${codeSizeDirectory}"
} }
if (flavor != null) {
args "-dFlavor=${flavor}"
}
if (extraGenSnapshotOptions != null) { if (extraGenSnapshotOptions != null) {
args "--ExtraGenSnapshotOptions=${extraGenSnapshotOptions}" args "--ExtraGenSnapshotOptions=${extraGenSnapshotOptions}"
} }
......
This diff is collapsed.
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../base/logger.dart'; import '../base/logger.dart';
import '../convert.dart'; import '../convert.dart';
import '../flutter_manifest.dart';
/// Represents a configured deferred component as defined in /// Represents a configured deferred component as defined in
/// the app's pubspec.yaml. /// the app's pubspec.yaml.
...@@ -12,7 +13,7 @@ class DeferredComponent { ...@@ -12,7 +13,7 @@ class DeferredComponent {
DeferredComponent({ DeferredComponent({
required this.name, required this.name,
this.libraries = const <String>[], this.libraries = const <String>[],
this.assets = const <Uri>[], this.assets = const <AssetsEntry>[],
}) : _assigned = false; }) : _assigned = false;
/// The name of the deferred component. There should be a matching /// The name of the deferred component. There should be a matching
...@@ -28,8 +29,8 @@ class DeferredComponent { ...@@ -28,8 +29,8 @@ class DeferredComponent {
/// libraries that are not listed here. /// libraries that are not listed here.
final List<String> libraries; final List<String> libraries;
/// Assets that are part of this component as a Uri relative to the project directory. /// Assets that are part of this component.
final List<Uri> assets; final List<AssetsEntry> assets;
/// The minimal set of [LoadingUnit]s needed that contain all of the dart libraries in /// The minimal set of [LoadingUnit]s needed that contain all of the dart libraries in
/// [libraries]. /// [libraries].
...@@ -95,8 +96,11 @@ class DeferredComponent { ...@@ -95,8 +96,11 @@ class DeferredComponent {
} }
} }
out.write('\n Assets:'); out.write('\n Assets:');
for (final Uri asset in assets) { for (final AssetsEntry asset in assets) {
out.write('\n - ${asset.path}'); out.write('\n - ${asset.uri.path}');
if (asset.flavors.isNotEmpty) {
out.write(' (flavors: ${asset.flavors.join(', ')})');
}
} }
return out.toString(); return out.toString();
} }
......
...@@ -286,6 +286,8 @@ class BuildInfo { ...@@ -286,6 +286,8 @@ class BuildInfo {
'PACKAGE_CONFIG': packagesPath, 'PACKAGE_CONFIG': packagesPath,
if (codeSizeDirectory != null) if (codeSizeDirectory != null)
'CODE_SIZE_DIRECTORY': codeSizeDirectory!, 'CODE_SIZE_DIRECTORY': codeSizeDirectory!,
if (flavor != null)
'FLAVOR': flavor!,
}; };
} }
...@@ -989,6 +991,9 @@ const String kBundleSkSLPath = 'BundleSkSLPath'; ...@@ -989,6 +991,9 @@ const String kBundleSkSLPath = 'BundleSkSLPath';
/// The define to pass build name /// The define to pass build name
const String kBuildName = 'BuildName'; const String kBuildName = 'BuildName';
/// The app flavor to build.
const String kFlavor = 'Flavor';
/// The define to pass build number /// The define to pass build number
const String kBuildNumber = 'BuildNumber'; const String kBuildNumber = 'BuildNumber';
......
...@@ -46,6 +46,7 @@ abstract class AndroidAssetBundle extends Target { ...@@ -46,6 +46,7 @@ abstract class AndroidAssetBundle extends Target {
if (buildModeEnvironment == null) { if (buildModeEnvironment == null) {
throw MissingDefineException(kBuildMode, name); throw MissingDefineException(kBuildMode, name);
} }
final BuildMode buildMode = BuildMode.fromCliName(buildModeEnvironment); final BuildMode buildMode = BuildMode.fromCliName(buildModeEnvironment);
final Directory outputDirectory = environment.outputDir final Directory outputDirectory = environment.outputDir
.childDirectory('flutter_assets') .childDirectory('flutter_assets')
...@@ -68,6 +69,7 @@ abstract class AndroidAssetBundle extends Target { ...@@ -68,6 +69,7 @@ abstract class AndroidAssetBundle extends Target {
targetPlatform: TargetPlatform.android, targetPlatform: TargetPlatform.android,
buildMode: buildMode, buildMode: buildMode,
shaderTarget: ShaderTarget.impellerAndroid, shaderTarget: ShaderTarget.impellerAndroid,
flavor: environment.defines[kFlavor],
); );
environment.depFileService.writeToFile( environment.depFileService.writeToFile(
assetDepfile, assetDepfile,
......
...@@ -34,6 +34,7 @@ Future<Depfile> copyAssets( ...@@ -34,6 +34,7 @@ Future<Depfile> copyAssets(
BuildMode? buildMode, BuildMode? buildMode,
required ShaderTarget shaderTarget, required ShaderTarget shaderTarget,
List<File> additionalInputs = const <File>[], List<File> additionalInputs = const <File>[],
String? flavor,
}) async { }) async {
// Check for an SkSL bundle. // Check for an SkSL bundle.
final String? shaderBundlePath = environment.defines[kBundleSkSLPath] ?? environment.inputs[kBundleSkSLPath]; final String? shaderBundlePath = environment.defines[kBundleSkSLPath] ?? environment.inputs[kBundleSkSLPath];
...@@ -58,6 +59,7 @@ Future<Depfile> copyAssets( ...@@ -58,6 +59,7 @@ Future<Depfile> copyAssets(
packagesPath: environment.projectDir.childFile('.packages').path, packagesPath: environment.projectDir.childFile('.packages').path,
deferredComponentsEnabled: environment.defines[kDeferredComponents] == 'true', deferredComponentsEnabled: environment.defines[kDeferredComponents] == 'true',
targetPlatform: targetPlatform, targetPlatform: targetPlatform,
flavor: flavor,
); );
if (resultCode != 0) { if (resultCode != 0) {
throw Exception('Failed to bundle asset files.'); throw Exception('Failed to bundle asset files.');
...@@ -323,6 +325,7 @@ class CopyAssets extends Target { ...@@ -323,6 +325,7 @@ class CopyAssets extends Target {
output, output,
targetPlatform: TargetPlatform.android, targetPlatform: TargetPlatform.android,
shaderTarget: ShaderTarget.sksl, shaderTarget: ShaderTarget.sksl,
flavor: environment.defines[kFlavor],
); );
environment.depFileService.writeToFile( environment.depFileService.writeToFile(
depfile, depfile,
......
...@@ -58,6 +58,8 @@ class CopyFlutterBundle extends Target { ...@@ -58,6 +58,8 @@ class CopyFlutterBundle extends Target {
if (buildModeEnvironment == null) { if (buildModeEnvironment == null) {
throw MissingDefineException(kBuildMode, 'copy_flutter_bundle'); throw MissingDefineException(kBuildMode, 'copy_flutter_bundle');
} }
final String? flavor = environment.defines[kFlavor];
final BuildMode buildMode = BuildMode.fromCliName(buildModeEnvironment); final BuildMode buildMode = BuildMode.fromCliName(buildModeEnvironment);
environment.outputDir.createSync(recursive: true); environment.outputDir.createSync(recursive: true);
...@@ -78,6 +80,7 @@ class CopyFlutterBundle extends Target { ...@@ -78,6 +80,7 @@ class CopyFlutterBundle extends Target {
targetPlatform: TargetPlatform.android, targetPlatform: TargetPlatform.android,
buildMode: buildMode, buildMode: buildMode,
shaderTarget: ShaderTarget.sksl, shaderTarget: ShaderTarget.sksl,
flavor: flavor,
); );
environment.depFileService.writeToFile( environment.depFileService.writeToFile(
assetDepfile, assetDepfile,
......
...@@ -533,6 +533,7 @@ abstract class IosAssetBundle extends Target { ...@@ -533,6 +533,7 @@ abstract class IosAssetBundle extends Target {
flutterProject.ios.infoPlist, flutterProject.ios.infoPlist,
flutterProject.ios.appFrameworkInfoPlist, flutterProject.ios.appFrameworkInfoPlist,
], ],
flavor: environment.defines[kFlavor],
); );
environment.depFileService.writeToFile( environment.depFileService.writeToFile(
assetDepfile, assetDepfile,
......
...@@ -393,6 +393,7 @@ abstract class MacOSBundleFlutterAssets extends Target { ...@@ -393,6 +393,7 @@ abstract class MacOSBundleFlutterAssets extends Target {
if (buildModeEnvironment == null) { if (buildModeEnvironment == null) {
throw MissingDefineException(kBuildMode, 'compile_macos_framework'); throw MissingDefineException(kBuildMode, 'compile_macos_framework');
} }
final BuildMode buildMode = BuildMode.fromCliName(buildModeEnvironment); final BuildMode buildMode = BuildMode.fromCliName(buildModeEnvironment);
final Directory frameworkRootDirectory = environment final Directory frameworkRootDirectory = environment
.outputDir .outputDir
...@@ -439,6 +440,7 @@ abstract class MacOSBundleFlutterAssets extends Target { ...@@ -439,6 +440,7 @@ abstract class MacOSBundleFlutterAssets extends Target {
assetDirectory, assetDirectory,
targetPlatform: TargetPlatform.darwin, targetPlatform: TargetPlatform.darwin,
shaderTarget: ShaderTarget.sksl, shaderTarget: ShaderTarget.sksl,
flavor: environment.defines[kFlavor],
); );
environment.depFileService.writeToFile( environment.depFileService.writeToFile(
assetDepfile, assetDepfile,
......
...@@ -114,6 +114,7 @@ Future<AssetBundle?> buildAssets({ ...@@ -114,6 +114,7 @@ Future<AssetBundle?> buildAssets({
String? assetDirPath, String? assetDirPath,
String? packagesPath, String? packagesPath,
TargetPlatform? targetPlatform, TargetPlatform? targetPlatform,
String? flavor,
}) async { }) async {
assetDirPath ??= getAssetBuildDirectory(); assetDirPath ??= getAssetBuildDirectory();
packagesPath ??= globals.fs.path.absolute('.packages'); packagesPath ??= globals.fs.path.absolute('.packages');
...@@ -124,6 +125,7 @@ Future<AssetBundle?> buildAssets({ ...@@ -124,6 +125,7 @@ Future<AssetBundle?> buildAssets({
manifestPath: manifestPath, manifestPath: manifestPath,
packagesPath: packagesPath, packagesPath: packagesPath,
targetPlatform: targetPlatform, targetPlatform: targetPlatform,
flavor: flavor,
); );
if (result != 0) { if (result != 0) {
return null; return null;
......
...@@ -231,29 +231,12 @@ class FlutterManifest { ...@@ -231,29 +231,12 @@ class FlutterManifest {
_logger.printError('Expected deferred component manifest to be a map.'); _logger.printError('Expected deferred component manifest to be a map.');
continue; 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( components.add(
DeferredComponent( DeferredComponent(
name: component['name'] as String, name: component['name'] as String,
libraries: component['libraries'] == null ? libraries: component['libraries'] == null ?
<String>[] : (component['libraries'] as List<dynamic>).cast<String>(), <String>[] : (component['libraries'] as List<dynamic>).cast<String>(),
assets: assetsUri, assets: _computeAssets(component['assets']),
) )
); );
} }
...@@ -311,26 +294,7 @@ class FlutterManifest { ...@@ -311,26 +294,7 @@ class FlutterManifest {
: fontList.map<Map<String, Object?>?>(castStringKeyedMap).whereType<Map<String, Object?>>().toList(); : fontList.map<Map<String, Object?>?>(castStringKeyedMap).whereType<Map<String, Object?>>().toList();
} }
late final List<Uri> assets = _computeAssets(); late final List<AssetsEntry> assets = _computeAssets(_flutterDescriptor['assets']);
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<Font> fonts = _extractFonts(); late final List<Font> fonts = _extractFonts();
...@@ -521,15 +485,7 @@ void _validateFlutter(YamlMap? yaml, List<String> errors) { ...@@ -521,15 +485,7 @@ void _validateFlutter(YamlMap? yaml, List<String> errors) {
errors.add('Expected "$yamlKey" to be a bool, but got $yamlValue (${yamlValue.runtimeType}).'); errors.add('Expected "$yamlKey" to be a bool, but got $yamlValue (${yamlValue.runtimeType}).');
} }
case 'assets': case 'assets':
if (yamlValue is! YamlList) { errors.addAll(_validateAssets(yamlValue));
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}).',
);
}
case 'shaders': case 'shaders':
if (yamlValue is! YamlList) { if (yamlValue is! YamlList) {
errors.add('Expected "$yamlKey" to be a list, but got $yamlValue (${yamlValue.runtimeType}).'); 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 ...@@ -640,17 +596,52 @@ void _validateDeferredComponents(MapEntry<Object?, Object?> kvp, List<String> er
} }
} }
if (valueMap.containsKey('assets')) { if (valueMap.containsKey('assets')) {
final Object? assets = valueMap['assets']; errors.addAll(_validateAssets(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');
}
} }
} }
} }
} }
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) { void _validateFonts(YamlList fonts, List<String> errors) {
const Set<int> fontWeights = <int>{ const Set<int> fontWeights = <int>{
100, 200, 300, 400, 500, 600, 700, 800, 900, 100, 200, 300, 400, 500, 600, 700, 800, 900,
...@@ -703,3 +694,103 @@ void _validateFonts(YamlList fonts, List<String> errors) { ...@@ -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);
}
...@@ -141,6 +141,8 @@ class HotRunner extends ResidentRunner { ...@@ -141,6 +141,8 @@ class HotRunner extends ResidentRunner {
NativeAssetsBuildRunner? _buildRunner; NativeAssetsBuildRunner? _buildRunner;
String? flavor;
Future<void> _calculateTargetPlatform() async { Future<void> _calculateTargetPlatform() async {
if (_targetPlatform != null) { if (_targetPlatform != null) {
return; return;
...@@ -494,7 +496,10 @@ class HotRunner extends ResidentRunner { ...@@ -494,7 +496,10 @@ class HotRunner extends ResidentRunner {
final bool rebuildBundle = assetBundle.needsBuild(); final bool rebuildBundle = assetBundle.needsBuild();
if (rebuildBundle) { if (rebuildBundle) {
globals.printTrace('Updating assets'); 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) { if (result != 0) {
return UpdateFSReport(); return UpdateFSReport();
} }
......
...@@ -1567,6 +1567,7 @@ class CapturingAppDomain extends AppDomain { ...@@ -1567,6 +1567,7 @@ class CapturingAppDomain extends AppDomain {
bool machine = true, bool machine = true,
String? userIdentifier, String? userIdentifier,
bool enableDevTools = true, bool enableDevTools = true,
String? flavor,
}) async { }) async {
this.multidexEnabled = multidexEnabled; this.multidexEnabled = multidexEnabled;
this.userIdentifier = userIdentifier; this.userIdentifier = userIdentifier;
......
...@@ -9,11 +9,15 @@ import 'package:file/memory.dart'; ...@@ -9,11 +9,15 @@ import 'package:file/memory.dart';
import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/asset.dart'; import 'package:flutter_tools/src/asset.dart';
import 'package:flutter_tools/src/base/file_system.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/platform.dart';
import 'package:flutter_tools/src/base/user_messages.dart';
import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/bundle_builder.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/devfs.dart';
import 'package:flutter_tools/src/globals.dart' as globals; 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 'package:standard_message_codec/standard_message_codec.dart';
import '../src/common.dart'; import '../src/common.dart';
...@@ -23,7 +27,9 @@ const String shaderLibDir = '/./shader_lib'; ...@@ -23,7 +27,9 @@ const String shaderLibDir = '/./shader_lib';
void main() { void main() {
group('AssetBundle.build', () { group('AssetBundle.build', () {
late Logger logger;
late FileSystem testFileSystem; late FileSystem testFileSystem;
late Platform platform;
setUp(() async { setUp(() async {
testFileSystem = MemoryFileSystem( testFileSystem = MemoryFileSystem(
...@@ -32,6 +38,8 @@ void main() { ...@@ -32,6 +38,8 @@ void main() {
: FileSystemStyle.posix, : FileSystemStyle.posix,
); );
testFileSystem.currentDirectory = testFileSystem.systemTempDirectory.createTempSync('flutter_asset_bundle_test.'); testFileSystem.currentDirectory = testFileSystem.systemTempDirectory.createTempSync('flutter_asset_bundle_test.');
logger = BufferLogger.test();
platform = FakePlatform(operatingSystem: globals.platform.operatingSystem);
}); });
testUsingContext('nonempty', () async { testUsingContext('nonempty', () async {
...@@ -323,6 +331,185 @@ flutter: ...@@ -323,6 +331,185 @@ flutter:
FileSystem: () => testFileSystem, FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(), 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)', () { group('AssetBundle.build (web builds)', () {
......
...@@ -6,6 +6,7 @@ import 'package:file/memory.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/deferred_component.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/flutter_manifest.dart';
import '../../src/common.dart'; import '../../src/common.dart';
...@@ -15,18 +16,27 @@ void main() { ...@@ -15,18 +16,27 @@ void main() {
final DeferredComponent component = DeferredComponent( final DeferredComponent component = DeferredComponent(
name: 'bestcomponent', name: 'bestcomponent',
libraries: <String>['lib1', 'lib2'], 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.name, 'bestcomponent');
expect(component.libraries, <String>['lib1', 'lib2']); 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', () { testWithoutContext('assignLoadingUnits selects the needed loading units and sets assigned', () {
final DeferredComponent component = DeferredComponent( final DeferredComponent component = DeferredComponent(
name: 'bestcomponent', name: 'bestcomponent',
libraries: <String>['lib1', 'lib2'], 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.libraries, <String>['lib1', 'lib2']);
expect(component.assigned, false); expect(component.assigned, false);
...@@ -94,7 +104,10 @@ void main() { ...@@ -94,7 +104,10 @@ void main() {
final DeferredComponent component = DeferredComponent( final DeferredComponent component = DeferredComponent(
name: 'bestcomponent', name: 'bestcomponent',
libraries: <String>['lib1', 'lib2'], 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'); expect(component.toString(), '\nDeferredComponent: bestcomponent\n Libraries:\n - lib1\n - lib2\n Assets:\n - asset1\n - asset2');
}); });
...@@ -103,7 +116,10 @@ void main() { ...@@ -103,7 +116,10 @@ void main() {
final DeferredComponent component = DeferredComponent( final DeferredComponent component = DeferredComponent(
name: 'bestcomponent', name: 'bestcomponent',
libraries: <String>['lib1', 'lib2'], 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'])]); 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'); expect(component.toString(), '\nDeferredComponent: bestcomponent\n Libraries:\n - lib1\n - lib2\n LoadingUnits:\n - 2\n Assets:\n - asset1\n - asset2');
......
...@@ -207,7 +207,7 @@ void main() { ...@@ -207,7 +207,7 @@ void main() {
}); });
testWithoutContext('toEnvironmentConfig encoding of standard values', () { testWithoutContext('toEnvironmentConfig encoding of standard values', () {
const BuildInfo buildInfo = BuildInfo(BuildMode.debug, '', const BuildInfo buildInfo = BuildInfo(BuildMode.debug, 'strawberry',
treeShakeIcons: true, treeShakeIcons: true,
trackWidgetCreation: true, trackWidgetCreation: true,
dartDefines: <String>['foo=2', 'bar=2'], dartDefines: <String>['foo=2', 'bar=2'],
...@@ -220,7 +220,7 @@ void main() { ...@@ -220,7 +220,7 @@ void main() {
packagesPath: 'foo/.dart_tool/package_config.json', packagesPath: 'foo/.dart_tool/package_config.json',
codeSizeDirectory: 'foo/code-size', codeSizeDirectory: 'foo/code-size',
// These values are ignored by toEnvironmentConfig // These values are ignored by toEnvironmentConfig
androidProjectArgs: <String>['foo=bar', 'fizz=bazz'] androidProjectArgs: <String>['foo=bar', 'fizz=bazz'],
); );
expect(buildInfo.toEnvironmentConfig(), <String, String>{ expect(buildInfo.toEnvironmentConfig(), <String, String>{
...@@ -235,6 +235,7 @@ void main() { ...@@ -235,6 +235,7 @@ void main() {
'BUNDLE_SKSL_PATH': 'foo/bar/baz.sksl.json', 'BUNDLE_SKSL_PATH': 'foo/bar/baz.sksl.json',
'PACKAGE_CONFIG': 'foo/.dart_tool/package_config.json', 'PACKAGE_CONFIG': 'foo/.dart_tool/package_config.json',
'CODE_SIZE_DIRECTORY': 'foo/code-size', 'CODE_SIZE_DIRECTORY': 'foo/code-size',
'FLAVOR': 'strawberry',
}); });
}); });
......
...@@ -32,6 +32,7 @@ void main() { ...@@ -32,6 +32,7 @@ void main() {
fileSystem: fileSystem, fileSystem: fileSystem,
logger: BufferLogger.test(), logger: BufferLogger.test(),
platform: FakePlatform(), platform: FakePlatform(),
defines: <String, String>{},
); );
fileSystem.file(environment.buildDir.childFile('app.dill')).createSync(recursive: true); fileSystem.file(environment.buildDir.childFile('app.dill')).createSync(recursive: true);
fileSystem.file('packages/flutter_tools/lib/src/build_system/targets/assets.dart') fileSystem.file('packages/flutter_tools/lib/src/build_system/targets/assets.dart')
...@@ -93,6 +94,69 @@ flutter: ...@@ -93,6 +94,69 @@ flutter:
ProcessManager: () => FakeProcessManager.any(), 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 { testUsingContext('Throws exception if pubspec contains missing files', () async {
fileSystem.file('pubspec.yaml') fileSystem.file('pubspec.yaml')
..createSync() ..createSync()
......
...@@ -733,7 +733,14 @@ class FakeBundle extends AssetBundle { ...@@ -733,7 +733,14 @@ class FakeBundle extends AssetBundle {
List<File> get additionalDependencies => <File>[]; List<File> get additionalDependencies => <File>[];
@override @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; return 0;
} }
......
...@@ -160,6 +160,7 @@ void main() { ...@@ -160,6 +160,7 @@ void main() {
'FRONTEND_SERVER_STARTER_PATH': frontendServerStarterPath, 'FRONTEND_SERVER_STARTER_PATH': frontendServerStarterPath,
'INFOPLIST_PATH': 'Info.plist', 'INFOPLIST_PATH': 'Info.plist',
'SDKROOT': sdkRoot, 'SDKROOT': sdkRoot,
'FLAVOR': 'strawberry',
'SPLIT_DEBUG_INFO': splitDebugInfo, 'SPLIT_DEBUG_INFO': splitDebugInfo,
'TRACK_WIDGET_CREATION': trackWidgetCreation, 'TRACK_WIDGET_CREATION': trackWidgetCreation,
'TREE_SHAKE_ICONS': treeShake, 'TREE_SHAKE_ICONS': treeShake,
...@@ -174,6 +175,7 @@ void main() { ...@@ -174,6 +175,7 @@ void main() {
'-dTargetPlatform=ios', '-dTargetPlatform=ios',
'-dTargetFile=lib/main.dart', '-dTargetFile=lib/main.dart',
'-dBuildMode=${buildMode.toLowerCase()}', '-dBuildMode=${buildMode.toLowerCase()}',
'-dFlavor=strawberry',
'-dIosArchs=$archs', '-dIosArchs=$archs',
'-dSdkRoot=$sdkRoot', '-dSdkRoot=$sdkRoot',
'-dSplitDebugInfo=$splitDebugInfo', '-dSplitDebugInfo=$splitDebugInfo',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment