[CP-stable]Roll forward: "Initialize default-app-flavor" (#169298) (#169623)

This pull request is created by [automatic cherry pick workflow](https://github.com/flutter/flutter/blob/main/docs/releases/Flutter-Cherrypick-Process.md#automatically-creates-a-cherry-pick-request)
Please fill in the form below, and a flutter domain expert will evaluate this cherry pick request.

### Issue Link:
What is the link to the issue this cherry-pick is addressing?

https://github.com/flutter/flutter/pull/169602

### Changelog Description:
Explain this cherry pick in one line that is accessible to most Flutter developers. See [best practices](https://github.com/flutter/flutter/blob/main/docs/releases/Hotfix-Documentation-Best-Practices.md) for examples

Fixes a bug where `appFlavor` is `null` when being run with `flutter test` or being hot-restarted.

### Impact Description:
What is the impact (ex. visual jank on Samsung phones, app crash, cannot ship an iOS app)? Does it impact development (ex. flutter doctor crashes when Android Studio is installed), or the shipping production app (the app crashes on launch)

Cannot reliably use `appFlavor` without rebuilding the app from scratch.

### Workaround:
Is there a workaround for this issue?

Do not use hot restart, do not use `flutter test`.

### Risk:
What is the risk level of this cherry-pick?

### Test Coverage:
Are you confident that your fix is well-tested by automated tests?

### Validation Steps:
What are the steps to validate that this fix works?

Automated test coverage.
This commit is contained in:
flutteractionsbot 2025-05-29 09:23:50 -07:00 committed by GitHub
parent 637479460d
commit 0a159b315d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 252 additions and 64 deletions

View File

@ -6,16 +6,14 @@ 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 platform, xcode;
import '../../globals.dart' as globals show xcode;
import '../../project.dart';
import '../../runner/flutter_command.dart';
import '../build_system.dart';
import '../depfile.dart';
import '../exceptions.dart';
@ -310,15 +308,17 @@ class KernelSnapshot extends Target {
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}',
);
}
// It is possible there is a flavor already in dartDefines, from another
// part of the build process, but this should take precedence as it happens
// last (xcodebuild execution).
//
// See https://github.com/flutter/flutter/issues/169598.
// If the flavor is already in the dart defines, remove it.
dartDefines.removeWhere((String define) => define.startsWith(kAppFlavor));
// Then, add it to the end.
dartDefines.add('$kAppFlavor=$flavor');
}
}

View File

@ -1438,6 +1438,18 @@ abstract class FlutterCommand extends Command<void> {
final String? cliFlavor = argParser.options.containsKey('flavor') ? stringArg('flavor') : null;
final String? flavor = cliFlavor ?? defaultFlavor;
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}',
);
}
if (flavor != null) {
dartDefines.add('$kAppFlavor=$flavor');
}
_addFlutterVersionToDartDefines(globals.flutterVersion, dartDefines);
return BuildInfo(

View File

@ -13,6 +13,7 @@ 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/convert.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:test/fake.dart';
@ -439,57 +440,6 @@ void main() {
},
);
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 {
@ -605,6 +555,63 @@ void main() {
},
);
testUsingContext(
'KernelSnapshot does not add kAppFlavor twice to Dart defines',
() 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,
);
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>[]),
'--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[kDartDefines] = base64Encode(utf8.encode('FLUTTER_APP_FLAVOR=vanilla'))
..defines[kFlavor] = 'strawberry'
..defines[kTrackWidgetCreation] = 'false',
);
expect(processManager, hasNoRemainingExpectations);
},
overrides: <Type, Generator>{
Platform: () => macPlatform,
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
},
);
testWithoutContext('KernelSnapshot does use track widget creation on debug builds', () async {
fileSystem.file('.dart_tool/package_config.json')
..createSync(recursive: true)

View File

@ -1270,6 +1270,98 @@ flutter:
);
});
testUsingContext(
"tool exits when $kAppFlavor is already set in user's environemnt",
() async {
final CommandRunner<void> runner = createTestCommandRunner(
_TestRunCommandThatOnlyValidates(),
);
expect(
runner.run(<String>['run', '--no-pub', '--no-hot']),
throwsToolExit(
message: '$kAppFlavor is used by the framework and cannot be set in the environment.',
),
);
},
overrides: <Type, Generator>{
DeviceManager:
() => FakeDeviceManager()..attachedDevices = <Device>[FakeDevice('name', 'id')],
FileSystem: () {
final MemoryFileSystem fileSystem = MemoryFileSystem.test();
fileSystem.file('lib/main.dart').createSync(recursive: true);
fileSystem.file('pubspec.yaml').createSync();
return fileSystem;
},
ProcessManager: FakeProcessManager.empty,
Platform: () => FakePlatform()..environment = <String, String>{kAppFlavor: 'AlreadySet'},
},
);
testUsingContext(
'tool exits when $kAppFlavor is set in --dart-define',
() async {
final CommandRunner<void> runner = createTestCommandRunner(
_TestRunCommandThatOnlyValidates(),
);
expect(
runner.run(<String>[
'run',
'--dart-define=$kAppFlavor=AlreadySet',
'--no-pub',
'--no-hot',
]),
throwsToolExit(
message: '$kAppFlavor is used by the framework and cannot be set using --dart-define',
),
);
},
overrides: <Type, Generator>{
DeviceManager:
() => FakeDeviceManager()..attachedDevices = <Device>[FakeDevice('name', 'id')],
FileSystem: () {
final MemoryFileSystem fileSystem = MemoryFileSystem.test();
fileSystem.file('lib/main.dart').createSync(recursive: true);
fileSystem.file('pubspec.yaml').createSync();
return fileSystem;
},
ProcessManager: FakeProcessManager.empty,
},
);
testUsingContext(
'tool exits when $kAppFlavor is set in --dart-define-from-file',
() async {
final CommandRunner<void> runner = createTestCommandRunner(
_TestRunCommandThatOnlyValidates(),
);
expect(
runner.run(<String>[
'run',
'--dart-define-from-file=config.json',
'--no-pub',
'--no-hot',
]),
throwsToolExit(
message: '$kAppFlavor is used by the framework and cannot be set using --dart-define',
),
);
},
overrides: <Type, Generator>{
DeviceManager:
() => FakeDeviceManager()..attachedDevices = <Device>[FakeDevice('name', 'id')],
FileSystem: () {
final MemoryFileSystem fileSystem = MemoryFileSystem.test();
fileSystem.file('lib/main.dart').createSync(recursive: true);
fileSystem.file('pubspec.yaml').createSync();
fileSystem.file('config.json')
..createSync()
..writeAsStringSync('{"$kAppFlavor": "AlreadySet"}');
return fileSystem;
},
ProcessManager: FakeProcessManager.empty,
},
);
group('Flutter version', () {
for (final String dartDefine in FlutterCommand.flutterVersionDartDefines) {
testUsingContext(

View File

@ -0,0 +1,74 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
@Tags(<String>['flutter-test-driver'])
library;
import 'package:flutter_tools/src/base/file_system.dart';
import '../src/common.dart';
import 'test_data/project.dart';
import 'test_driver.dart';
import 'test_utils.dart';
void main() {
final Project project = _DefaultFlavorProject();
late Directory tempDir;
late FlutterTestTestDriver flutter;
setUp(() async {
tempDir = createResolvedTempDirectorySync('default_flavor_test.');
await project.setUpIn(tempDir);
flutter = FlutterTestTestDriver(tempDir);
});
tearDown(() async {
tryToDelete(tempDir);
});
testWithoutContext('Reads "default-flavor" in "flutter test"', () async {
await flutter.test();
// Without an assertion, this test always passes.
final int? exitCode = await flutter.done;
expect(exitCode, 0, reason: 'flutter test failed with exit code $exitCode');
});
}
final class _DefaultFlavorProject extends Project {
@override
final String main = r'''
// Irrelevant to this test.
void main() {}
''';
@override
final String pubspec = r'''
name: test
environment:
sdk: ^3.7.0-0
flutter:
default-flavor: dev
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
''';
@override
final String test = r'''
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('receives default-flavor with flutter test', () async {
expect(appFlavor, 'dev');
});
}
''';
}

View File

@ -138,7 +138,10 @@ abstract final class FlutterTestDriver {
_stderr.stream.listen((String message) => _debugPrint(message, topic: '<=stderr='));
}
Future<void> get done async => _process?.exitCode;
/// Completes when process exits with the given exit code.
///
/// If the process has never been started, complets with `null`.
Future<int?> get done async => _process?.exitCode;
Future<void> connectToVmService({bool pauseOnExceptions = false}) async {
_vmService = await vmServiceConnectUri('$_vmServiceWsUri');