From 8d100a6416aa556a6dcde6f4331ac00431f66114 Mon Sep 17 00:00:00 2001 From: Victoria Ashworth <15619084+vashworth@users.noreply.github.com> Date: Wed, 19 Feb 2025 14:37:35 -0600 Subject: [PATCH] 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](https://github.com/flutter/flutter/blob/1d85de0fc802a640dff4fe91697f127d47f8cd21/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]. [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 --- dev/devicelab/bin/tasks/flavors_test_ios.dart | 60 +++++ .../bin/tasks/flavors_test_macos.dart | 62 +++++ dev/devicelab/lib/framework/ios.dart | 30 ++- .../lib/tasks/integration_tests.dart | 4 +- packages/flutter_tools/bin/macos_assemble.sh | 3 + packages/flutter_tools/bin/xcode_backend.dart | 3 + .../flutter_tools/lib/src/build_info.dart | 7 + .../lib/src/build_system/targets/common.dart | 44 +++- .../lib/src/build_system/targets/ios.dart | 3 +- .../lib/src/build_system/targets/macos.dart | 6 +- .../lib/src/runner/flutter_command.dart | 14 -- .../flutter_tools/lib/src/xcode_project.dart | 37 +++ .../build_system/targets/common_test.dart | 226 +++++++++++++++++- .../build_system/targets/ios_test.dart | 116 +++++++++ .../build_system/targets/macos_test.dart | 103 ++++++++ .../runner/flutter_command_test.dart | 116 --------- .../general.shard/xcode_backend_test.dart | 11 +- .../general.shard/xcode_project_test.dart | 109 ++++++++- 18 files changed, 811 insertions(+), 143 deletions(-) diff --git a/dev/devicelab/bin/tasks/flavors_test_ios.dart b/dev/devicelab/bin/tasks/flavors_test_ios.dart index a04cd60694..f2531c9bcc 100644 --- a/dev/devicelab/bin/tasks/flavors_test_ios.dart +++ b/dev/devicelab/bin/tasks/flavors_test_ios.dart @@ -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 main() async { return firstInstallFailure ?? TaskResult.success(null); }); + await _testFlavorWhenBuiltFromXcode(projectDir); + return installTestsResult; }); } @@ -97,3 +100,60 @@ Future _testInstallBogusFlavor() async { return TaskResult.success(null); } + +Future _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: ['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: ['clean', 'build'], + extraOptions: ['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: ['--flavor', 'paid', '--use-application-binary=$appPath'], + ).call(); +} diff --git a/dev/devicelab/bin/tasks/flavors_test_macos.dart b/dev/devicelab/bin/tasks/flavors_test_macos.dart index 75260c6f08..900acae902 100644 --- a/dev/devicelab/bin/tasks/flavors_test_macos.dart +++ b/dev/devicelab/bin/tasks/flavors_test_macos.dart @@ -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 main() async { return TaskResult.success(null); }); + await _testFlavorWhenBuiltFromXcode(projectDir); + return installTestsResult; }); } + +Future _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: ['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: ['clean', 'build'], + extraOptions: ['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: ['--flavor', 'paid', '--use-application-binary=$appPath'], + ).call(); +} diff --git a/dev/devicelab/lib/framework/ios.dart b/dev/devicelab/lib/framework/ios.dart index cfb34d2945..6b0c80114d 100644 --- a/dev/devicelab/lib/framework/ios.dart +++ b/dev/devicelab/lib/framework/ios.dart @@ -142,7 +142,32 @@ Future runXcodeTests({ required String platformDirectory, required String destination, required String testName, + List actions = const ['test'], String configuration = 'Release', + List extraOptions = const [], + 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 runXcodeBuild({ + required String platformDirectory, + required String destination, + required String testName, + List actions = const ['build'], + String configuration = 'Release', + List extraOptions = const [], + String scheme = 'Runner', bool skipCodesign = false, }) async { final Map environment = Platform.environment; @@ -170,14 +195,15 @@ Future 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', diff --git a/dev/devicelab/lib/tasks/integration_tests.dart b/dev/devicelab/lib/tasks/integration_tests.dart index 184a00b286..4687dc4ef8 100644 --- a/dev/devicelab/lib/tasks/integration_tests.dart +++ b/dev/devicelab/lib/tasks/integration_tests.dart @@ -22,11 +22,11 @@ TaskFunction createPlatformInteractionTest() { ).call; } -TaskFunction createFlavorsTest({Map? environment}) { +TaskFunction createFlavorsTest({Map? environment, List? extraOptions}) { return DriverTest( '${flutterDirectory.path}/dev/integration_tests/flavors', 'lib/main.dart', - extraOptions: ['--flavor', 'paid'], + extraOptions: extraOptions ?? ['--flavor', 'paid'], environment: environment, ).call; } diff --git a/packages/flutter_tools/bin/macos_assemble.sh b/packages/flutter_tools/bin/macos_assemble.sh index 90fd88506e..cf8da04de6 100755 --- a/packages/flutter_tools/bin/macos_assemble.sh +++ b/packages/flutter_tools/bin/macos_assemble.sh @@ -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 diff --git a/packages/flutter_tools/bin/xcode_backend.dart b/packages/flutter_tools/bin/xcode_backend.dart index b8d2e721c4..6e5982b54c 100644 --- a/packages/flutter_tools/bin/xcode_backend.dart +++ b/packages/flutter_tools/bin/xcode_backend.dart @@ -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'] ?? ''}', diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart index 99f8c3f128..ac6ce0e8ff 100644 --- a/packages/flutter_tools/lib/src/build_info.dart +++ b/packages/flutter_tools/lib/src/build_info.dart @@ -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'; diff --git a/packages/flutter_tools/lib/src/build_system/targets/common.dart b/packages/flutter_tools/lib/src/build_system/targets/common.dart index af46950557..b1f60007c2 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/common.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/common.dart @@ -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 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 _addFlavorToDartDefines( + List 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. diff --git a/packages/flutter_tools/lib/src/build_system/targets/ios.dart b/packages/flutter_tools/lib/src/build_system/targets/ios.dart index 2a4445d8bb..37331f279a 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/ios.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/ios.dart @@ -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, diff --git a/packages/flutter_tools/lib/src/build_system/targets/macos.dart b/packages/flutter_tools/lib/src/build_system/targets/macos.dart index cea8f13630..8ba45dd578 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/macos.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/macos.dart @@ -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: { 'NativeAssetsManifest.json': DevFSFileContent( environment.buildDir.childFile('native_assets.json'), diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart index 9e209c08a6..ac49f37053 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart @@ -1418,20 +1418,6 @@ abstract class FlutterCommand extends Command { 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, diff --git a/packages/flutter_tools/lib/src/xcode_project.dart b/packages/flutter_tools/lib/src/xcode_project.dart index 270d8dfe9b..f38073e91d 100644 --- a/packages/flutter_tools/lib/src/xcode_project.dart +++ b/packages/flutter_tools/lib/src/xcode_project.dart @@ -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 parseFlavorFromConfiguration(Environment environment) async { + final String? configuration = environment.defines[kXcodeConfiguration]; + final String? flavor = environment.defines[kFlavor]; + if (configuration == null) { + return flavor; + } + List 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. diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart index 303d09f0c7..2d7ebfcdf8 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart @@ -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( + command: [ + artifacts.getArtifactPath(Artifact.engineDartAotRuntime), + artifacts.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk), + '--sdk-root', + '$flutterPatchedSdkPath/', + '--target=flutter', + '--no-print-incremental-dependencies', + '-D$kAppFlavor=strawberry', + ...buildModeOptions(BuildMode.debug, []), + '--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 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: { + Platform: () => FakePlatform(environment: {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 buildResult = const KernelSnapshot().build( + androidEnvironment + ..defines[kTargetPlatform] = getNameForTargetPlatform(TargetPlatform.android) + ..defines[kBuildMode] = BuildMode.debug.cliName + ..defines[kFlavor] = 'strawberry' + ..defines[kDartDefines] = encodeDartDefines([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( + command: [ + artifacts.getArtifactPath(Artifact.engineDartAotRuntime), + artifacts.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk), + '--sdk-root', + '$flutterPatchedSdkPath/', + '--target=flutter', + '--no-print-incremental-dependencies', + '-D$kAppFlavor=chocolate', + ...buildModeOptions(BuildMode.debug, []), + '--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: { + XcodeProjectInterpreter: + () => FakeXcodeProjectInterpreter(schemes: ['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( + command: [ + artifacts.getArtifactPath(Artifact.engineDartAotRuntime), + artifacts.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk), + '--sdk-root', + '$flutterPatchedSdkPath/', + '--target=flutter', + '--no-print-incremental-dependencies', + '-D$kAppFlavor=chocolate', + ...buildModeOptions(BuildMode.debug, []), + '--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: { + XcodeProjectInterpreter: + () => FakeXcodeProjectInterpreter(schemes: ['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 ['Runner']}); + + @override + final bool isInstalled; + + List schemes; + + @override + Future getInfo(String projectPath, {String? projectFilename}) async { + return XcodeProjectInfo([], [], schemes, BufferLogger.test()); + } +} diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart index 704b9effd6..eae22b993d 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart @@ -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( + command: [ + 'xattr', + '-r', + '-d', + 'com.apple.FinderInfo', + frameworkDirectoryBinary.path, + ], + ), + FakeCommand( + command: [ + '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: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + Platform: () => macPlatform, + XcodeProjectInterpreter: + () => FakeXcodeProjectInterpreter(schemes: ['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 ['Runner']}); + + @override + final bool isInstalled; + + List schemes; + + @override + Future getInfo(String projectPath, {String? projectFilename}) async { + return XcodeProjectInfo([], [], schemes, BufferLogger.test()); + } +} diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart index 9e448e26ed..f23664832d 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart @@ -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: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + XcodeProjectInterpreter: + () => FakeXcodeProjectInterpreter(schemes: ['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 ['Runner']}); + + @override + final bool isInstalled; + + List schemes; + + @override + Future getInfo(String projectPath, {String? projectFilename}) async { + return XcodeProjectInfo([], [], schemes, BufferLogger.test()); + } +} diff --git a/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart b/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart index 877a1c2457..f4cf264833 100644 --- a/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart +++ b/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart @@ -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]; - final _TestRunCommandThatOnlyValidates command = _TestRunCommandThatOnlyValidates(); - final CommandRunner runner = createTestCommandRunner(command); - - expect( - runner.run(['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: { - DeviceManager: () => testDeviceManager, - Platform: - () => FakePlatform( - environment: {'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]; - final _TestRunCommandThatOnlyValidates command = _TestRunCommandThatOnlyValidates(); - final CommandRunner runner = createTestCommandRunner(command); - - expect( - runner.run([ - '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([ - '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: { - 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 devices = []; - - @override - List get deviceDiscoverers { - final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery(); - devices.forEach(discoverer.addDevice); - return [discoverer]; - } -} - -class _TestRunCommandThatOnlyValidates extends RunCommand { - @override - Future runCommand() async { - return FlutterCommandResult.success(); - } -} diff --git a/packages/flutter_tools/test/general.shard/xcode_backend_test.dart b/packages/flutter_tools/test/general.shard/xcode_backend_test.dart index 3b0a25b6b5..66aec4180c 100644 --- a/packages/flutter_tools/test/general.shard/xcode_backend_test.dart +++ b/packages/flutter_tools/test/general.shard/xcode_backend_test.dart @@ -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=', diff --git a/packages/flutter_tools/test/general.shard/xcode_project_test.dart b/packages/flutter_tools/test/general.shard/xcode_project_test.dart index f26d78ea33..aabfe82b7a 100644 --- a/packages/flutter_tools/test/general.shard/xcode_project_test.dart +++ b/packages/flutter_tools/test/general.shard/xcode_project_test.dart @@ -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: {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: {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: {kFlavor: 'strawberry', kXcodeConfiguration: 'Debug-vanilla'}, + ); + project.xcodeProject.createSync(recursive: true); + expect(await project.parseFlavorFromConfiguration(env), 'vanilla'); + }, + overrides: { + XcodeProjectInterpreter: + () => FakeXcodeProjectInterpreter(schemes: ['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: {kFlavor: 'strawberry', kXcodeConfiguration: 'Debug vanilla'}, + ); + project.xcodeProject.createSync(recursive: true); + expect(await project.parseFlavorFromConfiguration(env), 'vanilla'); + }, + overrides: { + XcodeProjectInterpreter: + () => FakeXcodeProjectInterpreter(schemes: ['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: {kFlavor: 'strawberry', kXcodeConfiguration: 'Debug-random'}, + ); + project.xcodeProject.createSync(recursive: true); + expect(await project.parseFlavorFromConfiguration(env), 'strawberry'); + }, + overrides: { + XcodeProjectInterpreter: + () => FakeXcodeProjectInterpreter(schemes: ['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 ['Runner'], + }); @override final bool isInstalled; @@ -373,9 +476,11 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete @override final Version? version; + List schemes; + @override Future getInfo(String projectPath, {String? projectFilename}) async { - return XcodeProjectInfo([], [], ['Runner'], BufferLogger.test()); + return XcodeProjectInfo([], [], schemes, BufferLogger.test()); } }