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