Get flavor/scheme in assemble command from the build configuration (#162907)

This moves the logic for `FLUTTER_APP_FLAVOR` into `flutter assemble`,
so that it also works when ran through Xcode and not just through the
Flutter CLI.

However, there's no definitive way to get the the flavor/scheme in
`flutter assemble`, so this makes a best effort to get it by parsing it
out of the `CONFIGURATION`. `CONFIGURATION` should have the name of the
scheme in it, although, this is only
[semi-enforced](1d85de0fc8/packages/flutter_tools/lib/src/ios/mac.dart (L201-L203)),
so may not always work. If it's unable to get the scheme name from the
`CONFIGURATION`, it falls back to using the `FLAVOR` environment
variable, which is set by the Flutter CLI and used currently.

Verified `Mac_ios flavors_test_ios` passes:
https://ci.chromium.org/ui/p/flutter/builders/prod.shadow/Mac_ios%20flavors_test_ios/7/overview

Verified `Mac flavors_test_macos` passes:
https://ci.chromium.org/ui/p/flutter/builders/try.shadow/Mac%20flavors_test_macos/2/overview

Fixes https://github.com/flutter/flutter/issues/155951.

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
Victoria Ashworth 2025-02-19 14:37:35 -06:00 committed by GitHub
parent 54b972084a
commit 8d100a6416
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 811 additions and 143 deletions

View File

@ -8,6 +8,7 @@ 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/ios.dart';
import 'package:flutter_devicelab/framework/task_result.dart';
import 'package:flutter_devicelab/framework/utils.dart';
import 'package:flutter_devicelab/tasks/integration_tests.dart';
@ -34,6 +35,8 @@ Future<void> main() async {
return firstInstallFailure ?? TaskResult.success(null);
});
await _testFlavorWhenBuiltFromXcode(projectDir);
return installTestsResult;
});
}
@ -97,3 +100,60 @@ Future<TaskResult> _testInstallBogusFlavor() async {
return TaskResult.success(null);
}
Future<TaskResult> _testFlavorWhenBuiltFromXcode(String projectDir) async {
final Device device = await devices.workingDevice;
await inDirectory(projectDir, () async {
// This will put FLAVOR=free in the Flutter/Generated.xcconfig file
await flutter(
'build',
options: <String>['ios', '--config-only', '--debug', '--flavor', 'free'],
);
});
final File generatedXcconfig = File(path.join(projectDir, 'ios/Flutter/Generated.xcconfig'));
if (!generatedXcconfig.existsSync()) {
throw TaskResult.failure('Unable to find Generated.xcconfig');
}
if (!generatedXcconfig.readAsStringSync().contains('FLAVOR=free')) {
throw TaskResult.failure('Generated.xcconfig does not contain FLAVOR=free');
}
const String configuration = 'Debug Paid';
const String productName = 'Paid App';
const String buildDir = 'build/ios';
// Delete app bundle before build to ensure checks below do not use previously
// built bundle.
final String appPath = '$projectDir/$buildDir/$configuration-iphoneos/$productName.app';
final Directory appBundle = Directory(appPath);
if (appBundle.existsSync()) {
appBundle.deleteSync(recursive: true);
}
if (!await runXcodeBuild(
platformDirectory: path.join(projectDir, 'ios'),
destination: 'id=${device.deviceId}',
testName: 'flavors_test_ios',
configuration: configuration,
scheme: 'paid',
actions: <String>['clean', 'build'],
extraOptions: <String>['BUILD_DIR=${path.join(projectDir, buildDir)}'],
)) {
throw TaskResult.failure('Build failed');
}
if (!appBundle.existsSync()) {
throw TaskResult.failure('App not found at $appPath');
}
if (!generatedXcconfig.readAsStringSync().contains('FLAVOR=free')) {
throw TaskResult.failure('Generated.xcconfig does not contain FLAVOR=free');
}
// Despite FLAVOR=free being in the Generated.xcconfig, the flavor found in
// the test should be "paid" because it was built with the "Debug Paid" configuration.
return createFlavorsTest(
extraOptions: <String>['--flavor', 'paid', '--use-application-binary=$appPath'],
).call();
}

View File

@ -7,6 +7,7 @@ import 'dart:typed_data';
import 'package:flutter_devicelab/framework/devices.dart';
import 'package:flutter_devicelab/framework/framework.dart';
import 'package:flutter_devicelab/framework/ios.dart';
import 'package:flutter_devicelab/framework/task_result.dart';
import 'package:flutter_devicelab/framework/utils.dart';
import 'package:flutter_devicelab/tasks/integration_tests.dart';
@ -83,6 +84,67 @@ Future<void> main() async {
return TaskResult.success(null);
});
await _testFlavorWhenBuiltFromXcode(projectDir);
return installTestsResult;
});
}
Future<TaskResult> _testFlavorWhenBuiltFromXcode(String projectDir) async {
await inDirectory(projectDir, () async {
// This will put FLAVOR=free in the Flutter/ephemeral/Flutter-Generated.xcconfig file
await flutter(
'build',
options: <String>['macos', '--config-only', '--debug', '--flavor', 'free'],
);
});
final File generatedXcconfig = File(
path.join(projectDir, 'macos/Flutter/ephemeral/Flutter-Generated.xcconfig'),
);
if (!generatedXcconfig.existsSync()) {
throw TaskResult.failure('Unable to find Generated.xcconfig');
}
if (!generatedXcconfig.readAsStringSync().contains('FLAVOR=free')) {
throw TaskResult.failure('Generated.xcconfig does not contain FLAVOR=free');
}
const String configuration = 'Debug-paid';
const String productName = 'Debug Paid';
const String buildDir = 'build/macos';
final String appPath = '$projectDir/$buildDir/$configuration/$productName.app';
// Delete app bundle before build to ensure checks below do not use previously
// built bundle.
final Directory appBundle = Directory(appPath);
if (appBundle.existsSync()) {
appBundle.deleteSync(recursive: true);
}
if (!await runXcodeBuild(
platformDirectory: path.join(projectDir, 'macos'),
destination: 'platform=macOS',
testName: 'flavors_test_macos',
configuration: configuration,
scheme: 'paid',
actions: <String>['clean', 'build'],
extraOptions: <String>['BUILD_DIR=${path.join(projectDir, buildDir)}'],
skipCodesign: true,
)) {
throw TaskResult.failure('Build failed');
}
if (!appBundle.existsSync()) {
throw TaskResult.failure('App not found at $appPath');
}
if (!generatedXcconfig.readAsStringSync().contains('FLAVOR=free')) {
throw TaskResult.failure('Generated.xcconfig does not contain FLAVOR=free');
}
// Despite FLAVOR=free being in the Generated.xcconfig, the flavor found in
// the test should be "paid" because it was built with the "Debug-paid" configuration.
return createFlavorsTest(
extraOptions: <String>['--flavor', 'paid', '--use-application-binary=$appPath'],
).call();
}

View File

@ -142,7 +142,32 @@ Future<bool> runXcodeTests({
required String platformDirectory,
required String destination,
required String testName,
List<String> actions = const <String>['test'],
String configuration = 'Release',
List<String> extraOptions = const <String>[],
String scheme = 'Runner',
bool skipCodesign = false,
}) {
return runXcodeBuild(
platformDirectory: platformDirectory,
destination: destination,
testName: testName,
actions: actions,
configuration: configuration,
extraOptions: extraOptions,
scheme: scheme,
skipCodesign: skipCodesign,
);
}
Future<bool> runXcodeBuild({
required String platformDirectory,
required String destination,
required String testName,
List<String> actions = const <String>['build'],
String configuration = 'Release',
List<String> extraOptions = const <String>[],
String scheme = 'Runner',
bool skipCodesign = false,
}) async {
final Map<String, String> environment = Platform.environment;
@ -170,14 +195,15 @@ Future<bool> runXcodeTests({
'-workspace',
'Runner.xcworkspace',
'-scheme',
'Runner',
scheme,
'-configuration',
configuration,
'-destination',
destination,
'-resultBundlePath',
resultBundlePath,
'test',
...actions,
...extraOptions,
'COMPILER_INDEX_STORE_ENABLE=NO',
if (developmentTeam != null) 'DEVELOPMENT_TEAM=$developmentTeam',
if (codeSignStyle != null) 'CODE_SIGN_STYLE=$codeSignStyle',

View File

@ -22,11 +22,11 @@ TaskFunction createPlatformInteractionTest() {
).call;
}
TaskFunction createFlavorsTest({Map<String, String>? environment}) {
TaskFunction createFlavorsTest({Map<String, String>? environment, List<String>? extraOptions}) {
return DriverTest(
'${flutterDirectory.path}/dev/integration_tests/flavors',
'lib/main.dart',
extraOptions: <String>['--flavor', 'paid'],
extraOptions: extraOptions ?? <String>['--flavor', 'paid'],
environment: environment,
).call;
}

View File

@ -141,6 +141,7 @@ BuildApp() {
"-dTrackWidgetCreation=${TRACK_WIDGET_CREATION}"
"-dAction=${ACTION}"
"-dFrontendServerStarterPath=${FRONTEND_SERVER_STARTER_PATH}"
"-dConfiguration=${CONFIGURATION}"
"--DartDefines=${DART_DEFINES}"
"--ExtraGenSnapshotOptions=${EXTRA_GEN_SNAPSHOT_OPTIONS}"
"--ExtraFrontEndOptions=${EXTRA_FRONT_END_OPTIONS}"
@ -162,6 +163,8 @@ BuildApp() {
flutter_args+=("-dPreBuildAction=PrepareFramework")
fi
# FLAVOR is set by the Flutter CLI in the Flutter/ephemeral/Flutter-Generated.xcconfig
# file when the --flavor flag is used, so it may not always be present.
if [[ -n "$FLAVOR" ]]; then
flutter_args+=("-dFlavor=${FLAVOR}")
fi

View File

@ -439,7 +439,10 @@ class Context {
'-dTargetPlatform=ios',
'-dTargetFile=$targetPath',
'-dBuildMode=$buildMode',
// FLAVOR is set by the Flutter CLI in the Flutter/Generated.xcconfig file
// when the --flavor flag is used, so it may not always be present.
if (environment['FLAVOR'] != null) '-dFlavor=${environment['FLAVOR']}',
'-dConfiguration=${environment['CONFIGURATION']}',
'-dIosArchs=$archs',
'-dSdkRoot=${environment['SDKROOT'] ?? ''}',
'-dSplitDebugInfo=${environment['SPLIT_DEBUG_INFO'] ?? ''}',

View File

@ -966,6 +966,13 @@ const String kBuildName = 'BuildName';
/// The app flavor to build.
const String kFlavor = 'Flavor';
/// Environment variable of the flavor to be set in dartDefines to be accessed
/// by the [appFlavor] service.
const String kAppFlavor = 'FLUTTER_APP_FLAVOR';
/// The Xcode configuration used to build the project.
const String kXcodeConfiguration = 'Configuration';
/// The define to pass build number
const String kBuildNumber = 'BuildNumber';

View File

@ -6,13 +6,16 @@ import 'package:package_config/package_config.dart';
import '../../artifacts.dart';
import '../../base/build.dart';
import '../../base/common.dart';
import '../../base/file_system.dart';
import '../../base/io.dart';
import '../../build_info.dart';
import '../../compile.dart';
import '../../dart/package_map.dart';
import '../../devfs.dart';
import '../../globals.dart' as globals show xcode;
import '../../globals.dart' as globals show platform, xcode;
import '../../project.dart';
import '../../runner/flutter_command.dart';
import '../build_system.dart';
import '../depfile.dart';
import '../exceptions.dart';
@ -251,6 +254,9 @@ class KernelSnapshot extends Target {
final String dillPath = environment.buildDir.childFile(dillName).path;
final List<String> dartDefines = decodeDartDefines(environment.defines, kDartDefines);
await _addFlavorToDartDefines(dartDefines, environment, targetPlatform);
final CompilerOutput? output = await compiler.compile(
sdkRoot: environment.artifacts.getArtifactPath(
Artifact.flutterPatchedSdkPath,
@ -271,7 +277,7 @@ class KernelSnapshot extends Target {
extraFrontEndOptions: extraFrontEndOptions,
fileSystemRoots: fileSystemRoots,
fileSystemScheme: fileSystemScheme,
dartDefines: decodeDartDefines(environment.defines, kDartDefines),
dartDefines: dartDefines,
packageConfig: packageConfig,
buildDir: environment.buildDir,
targetOS: targetOS,
@ -281,6 +287,40 @@ class KernelSnapshot extends Target {
throw Exception();
}
}
Future<void> _addFlavorToDartDefines(
List<String> dartDefines,
Environment environment,
TargetPlatform targetPlatform,
) async {
final String? flavor;
// For iOS and macOS projects, parse the flavor from the configuration, do
// not get from the FLAVOR environment variable. This is because when built
// from Xcode, the scheme/flavor can be changed through the UI and is not
// reflected in the environment variable.
if (targetPlatform == TargetPlatform.ios || targetPlatform == TargetPlatform.darwin) {
final FlutterProject flutterProject = FlutterProject.fromDirectory(environment.projectDir);
final XcodeBasedProject xcodeProject =
targetPlatform == TargetPlatform.ios ? flutterProject.ios : flutterProject.macos;
flavor = await xcodeProject.parseFlavorFromConfiguration(environment);
} else {
flavor = environment.defines[kFlavor];
}
if (flavor == null) {
return;
}
if (globals.platform.environment[kAppFlavor] != null) {
throwToolExit('$kAppFlavor is used by the framework and cannot be set in the environment.');
}
if (dartDefines.any((String define) => define.startsWith(kAppFlavor))) {
throwToolExit(
'$kAppFlavor is used by the framework and cannot be '
'set using --${FlutterOptions.kDartDefinesOption} or --${FlutterOptions.kDartDefineFromFileOption}',
);
}
dartDefines.add('$kAppFlavor=$flavor');
}
}
/// Supports compiling a dart kernel file to an ELF binary.

View File

@ -526,6 +526,7 @@ abstract class IosAssetBundle extends Target {
}
final FlutterProject flutterProject = FlutterProject.fromDirectory(environment.projectDir);
final String? flavor = await flutterProject.ios.parseFlavorFromConfiguration(environment);
// Copy the assets.
final Depfile assetDepfile = await copyAssets(
@ -542,7 +543,7 @@ abstract class IosAssetBundle extends Target {
environment.buildDir.childFile('native_assets.json'),
),
},
flavor: environment.defines[kFlavor],
flavor: flavor,
);
environment.depFileService.writeToFile(
assetDepfile,

View File

@ -12,6 +12,7 @@ import '../../base/process.dart';
import '../../build_info.dart';
import '../../devfs.dart';
import '../../globals.dart' as globals show xcode;
import '../../project.dart';
import '../build_system.dart';
import '../depfile.dart';
import '../exceptions.dart';
@ -487,12 +488,15 @@ abstract class MacOSBundleFlutterAssets extends Target {
.childDirectory('flutter_assets');
assetDirectory.createSync(recursive: true);
final FlutterProject flutterProject = FlutterProject.fromDirectory(environment.projectDir);
final String? flavor = await flutterProject.macos.parseFlavorFromConfiguration(environment);
final Depfile assetDepfile = await copyAssets(
environment,
assetDirectory,
targetPlatform: TargetPlatform.darwin,
buildMode: buildMode,
flavor: environment.defines[kFlavor],
flavor: flavor,
additionalContent: <String, DevFSContent>{
'NativeAssetsManifest.json': DevFSFileContent(
environment.buildDir.childFile('native_assets.json'),

View File

@ -1418,20 +1418,6 @@ abstract class FlutterCommand extends Command<void> {
final String? defaultFlavor = project.manifest.defaultFlavor;
final String? cliFlavor = argParser.options.containsKey('flavor') ? stringArg('flavor') : null;
final String? flavor = cliFlavor ?? defaultFlavor;
if (flavor != null) {
if (globals.platform.environment['FLUTTER_APP_FLAVOR'] != null) {
throwToolExit(
'FLUTTER_APP_FLAVOR is used by the framework and cannot be set in the environment.',
);
}
if (dartDefines.any((String define) => define.startsWith('FLUTTER_APP_FLAVOR'))) {
throwToolExit(
'FLUTTER_APP_FLAVOR is used by the framework and cannot be '
'set using --${FlutterOptions.kDartDefinesOption} or --${FlutterOptions.kDartDefineFromFileOption}',
);
}
dartDefines.add('FLUTTER_APP_FLAVOR=$flavor');
}
return BuildInfo(
buildMode,

View File

@ -7,6 +7,7 @@ import 'base/file_system.dart';
import 'base/utils.dart';
import 'base/version.dart';
import 'build_info.dart';
import 'build_system/build_system.dart';
import 'bundle.dart' as bundle;
import 'convert.dart';
import 'features.dart';
@ -283,6 +284,42 @@ abstract class XcodeBasedProject extends FlutterProjectPlatform {
}
return null;
}
/// When flutter assemble runs within an Xcode run script, it does not know
/// the scheme and therefore doesn't know what flavor is being used. This
/// makes a best effort to parse the scheme name from the [kXcodeConfiguration].
/// Most flavor's [kXcodeConfiguration] should follow the naming convention
/// of '$baseConfiguration-$scheme'. This is only semi-enforced by
/// [buildXcodeProject], so it may not work. Also check if separated by a
/// space instead of a `-`. Once parsed, match it with a scheme/flavor name.
/// If the flavor cannot be parsed or matched, use the [kFlavor] environment
/// variable, which may or may not be set/correct, as a fallback.
Future<String?> parseFlavorFromConfiguration(Environment environment) async {
final String? configuration = environment.defines[kXcodeConfiguration];
final String? flavor = environment.defines[kFlavor];
if (configuration == null) {
return flavor;
}
List<String> splitConfiguration = configuration.split('-');
if (splitConfiguration.length == 1) {
splitConfiguration = configuration.split(' ');
}
if (splitConfiguration.length == 1) {
return flavor;
}
final String parsedScheme = splitConfiguration[1];
final XcodeProjectInfo? info = await projectInfo();
if (info == null) {
return flavor;
}
for (final String schemeName in info.schemes) {
if (schemeName.toLowerCase() == parsedScheme.toLowerCase()) {
return schemeName;
}
}
return flavor;
}
}
/// Represents the iOS sub-project of a Flutter project.

View File

@ -13,6 +13,8 @@ import 'package:flutter_tools/src/build_system/exceptions.dart';
import 'package:flutter_tools/src/build_system/targets/common.dart';
import 'package:flutter_tools/src/build_system/targets/ios.dart';
import 'package:flutter_tools/src/compile.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:test/fake.dart';
import '../../../src/common.dart';
import '../../../src/context.dart';
@ -346,7 +348,7 @@ void main() {
expect(processManager, hasNoRemainingExpectations);
});
testWithoutContext(
testUsingContext(
'KernelSnapshot forces platform linking on debug for darwin target platforms',
() async {
fileSystem.file('.dart_tool/package_config.json')
@ -395,6 +397,214 @@ void main() {
},
);
testUsingContext(
'KernelSnapshot sets flavor in dartDefines if found in environment variable for non ios/darwin app',
() async {
fileSystem.file('.dart_tool/package_config.json')
..createSync(recursive: true)
..writeAsStringSync('{"configVersion": 2, "packages":[]}');
final String build = androidEnvironment.buildDir.path;
final String flutterPatchedSdkPath = artifacts.getArtifactPath(
Artifact.flutterPatchedSdkPath,
platform: TargetPlatform.android,
mode: BuildMode.debug,
);
processManager.addCommands(<FakeCommand>[
FakeCommand(
command: <String>[
artifacts.getArtifactPath(Artifact.engineDartAotRuntime),
artifacts.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk),
'--sdk-root',
'$flutterPatchedSdkPath/',
'--target=flutter',
'--no-print-incremental-dependencies',
'-D$kAppFlavor=strawberry',
...buildModeOptions(BuildMode.debug, <String>[]),
'--no-link-platform',
'--packages',
'/.dart_tool/package_config.json',
'--output-dill',
'$build/app.dill',
'--depfile',
'$build/kernel_snapshot_program.d',
'--incremental',
'--initialize-from-dill',
'$build/app.dill',
'--verbosity=error',
'file:///lib/main.dart',
],
stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n',
),
]);
},
);
testUsingContext(
"tool exits when $kAppFlavor is already set in user's environment",
() async {
fileSystem.file('.dart_tool/package_config.json')
..createSync(recursive: true)
..writeAsStringSync('{"configVersion": 2, "packages":[]}');
final Future<void> buildResult = const KernelSnapshot().build(
androidEnvironment
..defines[kTargetPlatform] = getNameForTargetPlatform(TargetPlatform.android)
..defines[kBuildMode] = BuildMode.debug.cliName
..defines[kFlavor] = 'strawberry'
..defines[kTrackWidgetCreation] = 'false',
);
expect(
buildResult,
throwsToolExit(
message: '$kAppFlavor is used by the framework and cannot be set in the environment.',
),
);
},
overrides: <Type, Generator>{
Platform: () => FakePlatform(environment: <String, String>{kAppFlavor: 'I was already set'}),
},
);
testUsingContext(
'tool exits when $kAppFlavor is set in --dart-define or --dart-define-from-file',
() async {
fileSystem.file('.dart_tool/package_config.json')
..createSync(recursive: true)
..writeAsStringSync('{"configVersion": 2, "packages":[]}');
final Future<void> buildResult = const KernelSnapshot().build(
androidEnvironment
..defines[kTargetPlatform] = getNameForTargetPlatform(TargetPlatform.android)
..defines[kBuildMode] = BuildMode.debug.cliName
..defines[kFlavor] = 'strawberry'
..defines[kDartDefines] = encodeDartDefines(<String>[kAppFlavor, 'strawberry'])
..defines[kTrackWidgetCreation] = 'false',
);
expect(
buildResult,
throwsToolExit(
message:
'$kAppFlavor is used by the framework and cannot be set using --dart-define or --dart-define-from-file',
),
);
},
);
testUsingContext(
'KernelSnapshot sets flavor in dartDefines from Xcode build configuration if ios app',
() async {
fileSystem.file('.dart_tool/package_config.json')
..createSync(recursive: true)
..writeAsStringSync('{"configVersion": 2, "packages":[]}');
final String build = iosEnvironment.buildDir.path;
final String flutterPatchedSdkPath = artifacts.getArtifactPath(
Artifact.flutterPatchedSdkPath,
platform: TargetPlatform.ios,
mode: BuildMode.debug,
);
fileSystem.directory('/ios/Runner.xcodeproj').createSync(recursive: true);
processManager.addCommands(<FakeCommand>[
FakeCommand(
command: <String>[
artifacts.getArtifactPath(Artifact.engineDartAotRuntime),
artifacts.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk),
'--sdk-root',
'$flutterPatchedSdkPath/',
'--target=flutter',
'--no-print-incremental-dependencies',
'-D$kAppFlavor=chocolate',
...buildModeOptions(BuildMode.debug, <String>[]),
'--no-link-platform',
'--packages',
'/.dart_tool/package_config.json',
'--output-dill',
'$build/app.dill',
'--depfile',
'$build/kernel_snapshot_program.d',
'--incremental',
'--initialize-from-dill',
'$build/app.dill',
'--verbosity=error',
'file:///lib/main.dart',
],
stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n',
),
]);
await const KernelSnapshot().build(
iosEnvironment
..defines[kTargetPlatform] = getNameForTargetPlatform(TargetPlatform.ios)
..defines[kBuildMode] = BuildMode.debug.cliName
..defines[kFlavor] = 'strawberry'
..defines[kXcodeConfiguration] = 'Debug-chocolate'
..defines[kTrackWidgetCreation] = 'false',
);
expect(processManager, hasNoRemainingExpectations);
},
overrides: <Type, Generator>{
XcodeProjectInterpreter:
() => FakeXcodeProjectInterpreter(schemes: <String>['Runner', 'chocolate']),
},
);
testUsingContext(
'KernelSnapshot sets flavor in dartDefines from Xcode build configuration if macos app',
() async {
fileSystem.file('.dart_tool/package_config.json')
..createSync(recursive: true)
..writeAsStringSync('{"configVersion": 2, "packages":[]}');
final String build = iosEnvironment.buildDir.path;
final String flutterPatchedSdkPath = artifacts.getArtifactPath(
Artifact.flutterPatchedSdkPath,
platform: TargetPlatform.darwin,
mode: BuildMode.debug,
);
fileSystem.directory('/macos/Runner.xcodeproj').createSync(recursive: true);
processManager.addCommands(<FakeCommand>[
FakeCommand(
command: <String>[
artifacts.getArtifactPath(Artifact.engineDartAotRuntime),
artifacts.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk),
'--sdk-root',
'$flutterPatchedSdkPath/',
'--target=flutter',
'--no-print-incremental-dependencies',
'-D$kAppFlavor=chocolate',
...buildModeOptions(BuildMode.debug, <String>[]),
'--packages',
'/.dart_tool/package_config.json',
'--output-dill',
'$build/app.dill',
'--depfile',
'$build/kernel_snapshot_program.d',
'--incremental',
'--initialize-from-dill',
'$build/app.dill',
'--verbosity=error',
'file:///lib/main.dart',
],
stdout: 'result $kBoundaryKey\n$kBoundaryKey\n$kBoundaryKey $build/app.dill 0\n',
),
]);
await const KernelSnapshot().build(
iosEnvironment
..defines[kTargetPlatform] = getNameForTargetPlatform(TargetPlatform.darwin)
..defines[kBuildMode] = BuildMode.debug.cliName
..defines[kFlavor] = 'strawberry'
..defines[kXcodeConfiguration] = 'Debug-chocolate'
..defines[kTrackWidgetCreation] = 'false',
);
expect(processManager, hasNoRemainingExpectations);
},
overrides: <Type, Generator>{
XcodeProjectInterpreter:
() => FakeXcodeProjectInterpreter(schemes: <String>['Runner', 'chocolate']),
},
);
testWithoutContext('KernelSnapshot does use track widget creation on debug builds', () async {
fileSystem.file('.dart_tool/package_config.json')
..createSync(recursive: true)
@ -707,3 +917,17 @@ void main() {
expect(processManager, hasNoRemainingExpectations);
});
}
class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter {
FakeXcodeProjectInterpreter({this.isInstalled = true, this.schemes = const <String>['Runner']});
@override
final bool isInstalled;
List<String> schemes;
@override
Future<XcodeProjectInfo?> getInfo(String projectPath, {String? projectFilename}) async {
return XcodeProjectInfo(<String>[], <String>[], schemes, BufferLogger.test());
}
}

View File

@ -11,7 +11,9 @@ import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/targets/ios.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:test/fake.dart';
import 'package:unified_analytics/unified_analytics.dart';
import '../../../src/common.dart';
@ -267,6 +269,106 @@ void main() {
},
);
testUsingContext(
'DebugIosApplicationBundle with flavor',
() async {
environment.defines[kBuildMode] = 'debug';
environment.defines[kCodesignIdentity] = 'ABC123';
environment.defines[kFlavor] = 'vanilla';
environment.defines[kXcodeConfiguration] = 'Debug-strawberry';
fileSystem.directory('/ios/Runner.xcodeproj').createSync(recursive: true);
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);
// Precompiled dart data
fileSystem
.file(artifacts.getArtifactPath(Artifact.vmSnapshotData, mode: BuildMode.debug))
.createSync();
fileSystem
.file(artifacts.getArtifactPath(Artifact.isolateSnapshotData, mode: BuildMode.debug))
.createSync();
// Project info
fileSystem
.directory('.dart_tool')
.childFile('package_config.json')
.createSync(recursive: true);
// Plist file
fileSystem
.file(fileSystem.path.join('ios', 'Flutter', 'AppFrameworkInfo.plist'))
.createSync(recursive: true);
// App kernel
environment.buildDir.childFile('app.dill').createSync(recursive: true);
environment.buildDir.childFile('native_assets.json').createSync();
// Stub framework
environment.buildDir
.childDirectory('App.framework')
.childFile('App')
.createSync(recursive: true);
final Directory frameworkDirectory = environment.outputDir.childDirectory('App.framework');
final File frameworkDirectoryBinary = frameworkDirectory.childFile('App');
processManager.addCommands(<FakeCommand>[
FakeCommand(
command: <String>[
'xattr',
'-r',
'-d',
'com.apple.FinderInfo',
frameworkDirectoryBinary.path,
],
),
FakeCommand(
command: <String>[
'codesign',
'--force',
'--sign',
'ABC123',
'--timestamp=none',
frameworkDirectoryBinary.path,
],
),
]);
await const DebugIosApplicationBundle().build(environment);
expect(
fileSystem.file('${frameworkDirectory.path}/flutter_assets/assets/common/image.png'),
exists,
);
expect(
fileSystem.file('${frameworkDirectory.path}/flutter_assets/assets/vanilla/ice-cream.png'),
isNot(exists),
);
expect(
fileSystem.file(
'${frameworkDirectory.path}/flutter_assets/assets/strawberry/ice-cream.png',
),
exists,
);
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
Platform: () => macPlatform,
XcodeProjectInterpreter:
() => FakeXcodeProjectInterpreter(schemes: <String>['Runner', 'strawberry']),
},
);
testUsingContext(
'DebugIosApplicationBundle with impeller and shader compilation',
() async {
@ -1058,3 +1160,17 @@ void main() {
});
});
}
class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter {
FakeXcodeProjectInterpreter({this.isInstalled = true, this.schemes = const <String>['Runner']});
@override
final bool isInstalled;
List<String> schemes;
@override
Future<XcodeProjectInfo?> getInfo(String projectPath, {String? projectFilename}) async {
return XcodeProjectInfo(<String>[], <String>[], schemes, BufferLogger.test());
}
}

View File

@ -10,6 +10,8 @@ import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/targets/macos.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:test/fake.dart';
import 'package:unified_analytics/unified_analytics.dart';
import '../../../src/common.dart';
@ -465,6 +467,93 @@ void main() {
},
);
testUsingContext(
'debug macOS application copies correct assets with flavor',
() async {
fileSystem
.directory(
artifacts.getArtifactPath(Artifact.flutterMacOSFramework, mode: BuildMode.debug),
)
.createSync();
fileSystem
.file(
artifacts.getArtifactPath(
Artifact.vmSnapshotData,
platform: TargetPlatform.darwin,
mode: BuildMode.debug,
),
)
.createSync(recursive: true);
fileSystem
.file(
artifacts.getArtifactPath(
Artifact.isolateSnapshotData,
platform: TargetPlatform.darwin,
mode: BuildMode.debug,
),
)
.createSync(recursive: true);
fileSystem.file('${environment.buildDir.path}/App.framework/App').createSync(recursive: true);
final String inputKernel = '${environment.buildDir.path}/app.dill';
fileSystem.file(inputKernel)
..createSync(recursive: true)
..writeAsStringSync('testing');
environment.buildDir.childFile('native_assets.json').createSync();
environment.defines[kXcodeConfiguration] = 'Debug-strawberry';
fileSystem.directory('/macos/Runner.xcodeproj').createSync(recursive: true);
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);
fileSystem
.directory('.dart_tool')
.childFile('package_config.json')
.createSync(recursive: true);
await const DebugMacOSBundleFlutterAssets().build(environment);
final Directory frameworkDirectory = environment.outputDir.childDirectory(
'/App.framework/Versions/A/Resources/',
);
expect(
fileSystem.file('${frameworkDirectory.path}/flutter_assets/assets/common/image.png'),
exists,
);
expect(
fileSystem.file('${frameworkDirectory.path}/flutter_assets/assets/vanilla/ice-cream.png'),
isNot(exists),
);
expect(
fileSystem.file(
'${frameworkDirectory.path}/flutter_assets/assets/strawberry/ice-cream.png',
),
exists,
);
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
XcodeProjectInterpreter:
() => FakeXcodeProjectInterpreter(schemes: <String>['Runner', 'strawberry']),
},
);
testUsingContext(
'release/profile macOS application has no blob or precompiled runtime',
() async {
@ -883,3 +972,17 @@ void main() {
},
);
}
class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter {
FakeXcodeProjectInterpreter({this.isInstalled = true, this.schemes = const <String>['Runner']});
@override
final bool isInstalled;
List<String> schemes;
@override
Future<XcodeProjectInfo?> getInfo(String projectPath, {String? projectFilename}) async {
return XcodeProjectInfo(<String>[], <String>[], schemes, BufferLogger.test());
}
}

View File

@ -18,7 +18,6 @@ import 'package:flutter_tools/src/base/time.dart';
import 'package:flutter_tools/src/base/user_messages.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/run.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/pre_run_validator.dart';
@ -1209,108 +1208,12 @@ void main() {
});
group('--flavor', () {
late _TestDeviceManager testDeviceManager;
late Logger logger;
late FileSystem fileSystem;
setUp(() {
logger = BufferLogger.test();
testDeviceManager = _TestDeviceManager(logger: logger);
fileSystem = MemoryFileSystem.test();
});
testUsingContext(
"tool exits when FLUTTER_APP_FLAVOR is already set in user's environment",
() async {
fileSystem.file('lib/main.dart').createSync(recursive: true);
fileSystem.file('pubspec.yaml').createSync();
final FakeDevice device = FakeDevice(
'name',
'id',
type: PlatformType.android,
supportsFlavors: true,
);
testDeviceManager.devices = <Device>[device];
final _TestRunCommandThatOnlyValidates command = _TestRunCommandThatOnlyValidates();
final CommandRunner<void> runner = createTestCommandRunner(command);
expect(
runner.run(<String>['run', '--no-pub', '--no-hot', '--flavor=strawberry']),
throwsToolExit(
message:
'FLUTTER_APP_FLAVOR is used by the framework and cannot be set in the environment.',
),
);
},
overrides: <Type, Generator>{
DeviceManager: () => testDeviceManager,
Platform:
() => FakePlatform(
environment: <String, String>{'FLUTTER_APP_FLAVOR': 'I was already set'},
),
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
},
);
testUsingContext(
'tool exits when FLUTTER_APP_FLAVOR is set in --dart-define or --dart-define-from-file',
() async {
fileSystem.file('lib/main.dart').createSync(recursive: true);
fileSystem.file('pubspec.yaml').createSync();
fileSystem.file('config.json')
..createSync()
..writeAsStringSync('{"FLUTTER_APP_FLAVOR": "strawberry"}');
final FakeDevice device = FakeDevice(
'name',
'id',
type: PlatformType.android,
supportsFlavors: true,
);
testDeviceManager.devices = <Device>[device];
final _TestRunCommandThatOnlyValidates command = _TestRunCommandThatOnlyValidates();
final CommandRunner<void> runner = createTestCommandRunner(command);
expect(
runner.run(<String>[
'run',
'--dart-define=FLUTTER_APP_FLAVOR=strawberry',
'--no-pub',
'--no-hot',
'--flavor=strawberry',
]),
throwsToolExit(
message:
'FLUTTER_APP_FLAVOR is used by the framework and cannot be set using --dart-define or --dart-define-from-file',
),
);
expect(
runner.run(<String>[
'run',
'--dart-define-from-file=config.json',
'--no-pub',
'--no-hot',
'--flavor=strawberry',
]),
throwsToolExit(
message:
'FLUTTER_APP_FLAVOR is used by the framework and cannot be set using --dart-define or --dart-define-from-file',
),
);
},
overrides: <Type, Generator>{
DeviceManager: () => testDeviceManager,
Platform: () => FakePlatform(),
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
},
);
testUsingContext(
'CLI option overrides default flavor from manifest',
() async {
@ -1475,22 +1378,3 @@ class FakeClock extends Fake implements SystemClock {
return DateTime.fromMillisecondsSinceEpoch(times.removeAt(0));
}
}
class _TestDeviceManager extends DeviceManager {
_TestDeviceManager({required super.logger});
List<Device> devices = <Device>[];
@override
List<DeviceDiscovery> get deviceDiscoverers {
final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
devices.forEach(discoverer.addDevice);
return <DeviceDiscovery>[discoverer];
}
}
class _TestRunCommandThatOnlyValidates extends RunCommand {
@override
Future<FlutterCommandResult> runCommand() async {
return FlutterCommandResult.success();
}
}

View File

@ -89,6 +89,7 @@ void main() {
'-dTargetPlatform=ios',
'-dTargetFile=lib/main.dart',
'-dBuildMode=${buildMode.toLowerCase()}',
'-dConfiguration=$buildMode',
'-dIosArchs=',
'-dSdkRoot=',
'-dSplitDebugInfo=',
@ -139,7 +140,7 @@ void main() {
'ARCHS': archs,
'BUILT_PRODUCTS_DIR': buildDir.path,
'CODE_SIGNING_REQUIRED': 'YES',
'CONFIGURATION': buildMode,
'CONFIGURATION': '$buildMode-strawberry',
'DART_DEFINES': dartDefines,
'DART_OBFUSCATION': dartObfuscation,
'EXPANDED_CODE_SIGN_IDENTITY': expandedCodeSignIdentity,
@ -165,6 +166,7 @@ void main() {
'-dTargetFile=lib/main.dart',
'-dBuildMode=${buildMode.toLowerCase()}',
'-dFlavor=strawberry',
'-dConfiguration=$buildMode-strawberry',
'-dIosArchs=$archs',
'-dSdkRoot=$sdkRoot',
'-dSplitDebugInfo=$splitDebugInfo',
@ -284,6 +286,7 @@ void main() {
'-dTargetPlatform=ios',
'-dTargetFile=lib/main.dart',
'-dBuildMode=${buildMode.toLowerCase()}',
'-dConfiguration=$buildMode',
'-dIosArchs=',
'-dSdkRoot=',
'-dSplitDebugInfo=',
@ -330,7 +333,6 @@ void main() {
'ARCHS': archs,
'BUILT_PRODUCTS_DIR': buildDir.path,
'CODE_SIGNING_REQUIRED': 'YES',
'CONFIGURATION': buildMode,
'DART_DEFINES': dartDefines,
'DART_OBFUSCATION': dartObfuscation,
'EXPANDED_CODE_SIGN_IDENTITY': expandedCodeSignIdentity,
@ -340,6 +342,7 @@ void main() {
'FRONTEND_SERVER_STARTER_PATH': frontendServerStarterPath,
'INFOPLIST_PATH': 'Info.plist',
'SDKROOT': sdkRoot,
'CONFIGURATION': '$buildMode-strawberry',
'FLAVOR': 'strawberry',
'SPLIT_DEBUG_INFO': splitDebugInfo,
'TRACK_WIDGET_CREATION': trackWidgetCreation,
@ -356,6 +359,7 @@ void main() {
'-dTargetFile=lib/main.dart',
'-dBuildMode=${buildMode.toLowerCase()}',
'-dFlavor=strawberry',
'-dConfiguration=$buildMode-strawberry',
'-dIosArchs=$archs',
'-dSdkRoot=$sdkRoot',
'-dSplitDebugInfo=$splitDebugInfo',
@ -406,6 +410,7 @@ void main() {
'-dTargetPlatform=ios',
'-dTargetFile=lib/main.dart',
'-dBuildMode=${buildMode.toLowerCase()}',
'-dConfiguration=$buildMode',
'-dIosArchs=arm64',
'-dSdkRoot=',
'-dSplitDebugInfo=',
@ -456,6 +461,7 @@ void main() {
'-dTargetPlatform=ios',
'-dTargetFile=lib/main.dart',
'-dBuildMode=${buildMode.toLowerCase()}',
'-dConfiguration=$buildMode',
'-dIosArchs=arm64',
'-dSdkRoot=',
'-dSplitDebugInfo=',
@ -505,6 +511,7 @@ void main() {
'-dTargetPlatform=ios',
'-dTargetFile=lib/main.dart',
'-dBuildMode=${buildMode.toLowerCase()}',
'-dConfiguration=$buildMode',
'-dIosArchs=arm64 x86_64',
'-dSdkRoot=',
'-dSplitDebugInfo=',

View File

@ -4,9 +4,12 @@
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/version.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/features.dart';
import 'package:flutter_tools/src/flutter_manifest.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart';
@ -216,6 +219,102 @@ void main() {
},
);
});
group('parseFlavorFromConfiguration', () {
testWithoutContext('from FLAVOR when CONFIGURATION is null', () async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final IosProject project = IosProject.fromFlutter(FakeFlutterProject(fileSystem: fs));
final Environment env = Environment.test(
fs.currentDirectory,
fileSystem: fs,
logger: BufferLogger.test(),
artifacts: Artifacts.test(),
processManager: FakeProcessManager.any(),
defines: <String, String>{kFlavor: 'strawberry'},
);
expect(await project.parseFlavorFromConfiguration(env), 'strawberry');
});
testWithoutContext('from FLAVOR when CONFIGURATION is does not contain delimiter', () async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final IosProject project = IosProject.fromFlutter(FakeFlutterProject(fileSystem: fs));
final Environment env = Environment.test(
fs.currentDirectory,
fileSystem: fs,
logger: BufferLogger.test(),
artifacts: Artifacts.test(),
processManager: FakeProcessManager.any(),
defines: <String, String>{kFlavor: 'strawberry', kXcodeConfiguration: 'Debug'},
);
expect(await project.parseFlavorFromConfiguration(env), 'strawberry');
});
testUsingContext(
'from CONFIGURATION when has flavor following a hyphen that matches a scheme',
() async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final IosProject project = IosProject.fromFlutter(FakeFlutterProject(fileSystem: fs));
final Environment env = Environment.test(
fs.currentDirectory,
fileSystem: fs,
logger: BufferLogger.test(),
artifacts: Artifacts.test(),
processManager: FakeProcessManager.any(),
defines: <String, String>{kFlavor: 'strawberry', kXcodeConfiguration: 'Debug-vanilla'},
);
project.xcodeProject.createSync(recursive: true);
expect(await project.parseFlavorFromConfiguration(env), 'vanilla');
},
overrides: <Type, Generator>{
XcodeProjectInterpreter:
() => FakeXcodeProjectInterpreter(schemes: <String>['Runner', 'vanilla']),
},
);
testUsingContext(
'from CONFIGURATION when has flavor following a space that matches a scheme',
() async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final IosProject project = IosProject.fromFlutter(FakeFlutterProject(fileSystem: fs));
final Environment env = Environment.test(
fs.currentDirectory,
fileSystem: fs,
logger: BufferLogger.test(),
artifacts: Artifacts.test(),
processManager: FakeProcessManager.any(),
defines: <String, String>{kFlavor: 'strawberry', kXcodeConfiguration: 'Debug vanilla'},
);
project.xcodeProject.createSync(recursive: true);
expect(await project.parseFlavorFromConfiguration(env), 'vanilla');
},
overrides: <Type, Generator>{
XcodeProjectInterpreter:
() => FakeXcodeProjectInterpreter(schemes: <String>['Runner', 'vanilla']),
},
);
testUsingContext(
'from FLAVOR when CONFIGURATION does not match a scheme',
() async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final IosProject project = IosProject.fromFlutter(FakeFlutterProject(fileSystem: fs));
final Environment env = Environment.test(
fs.currentDirectory,
fileSystem: fs,
logger: BufferLogger.test(),
artifacts: Artifacts.test(),
processManager: FakeProcessManager.any(),
defines: <String, String>{kFlavor: 'strawberry', kXcodeConfiguration: 'Debug-random'},
);
project.xcodeProject.createSync(recursive: true);
expect(await project.parseFlavorFromConfiguration(env), 'strawberry');
},
overrides: <Type, Generator>{
XcodeProjectInterpreter:
() => FakeXcodeProjectInterpreter(schemes: <String>['Runner', 'vanilla']),
},
);
});
});
group('MacOSProject', () {
@ -365,7 +464,11 @@ class FakeFlutterProject extends Fake implements FlutterProject {
}
class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter {
FakeXcodeProjectInterpreter({this.isInstalled = true, this.version});
FakeXcodeProjectInterpreter({
this.isInstalled = true,
this.version,
this.schemes = const <String>['Runner'],
});
@override
final bool isInstalled;
@ -373,9 +476,11 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete
@override
final Version? version;
List<String> schemes;
@override
Future<XcodeProjectInfo?> getInfo(String projectPath, {String? projectFilename}) async {
return XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], BufferLogger.test());
return XcodeProjectInfo(<String>[], <String>[], schemes, BufferLogger.test());
}
}