Skip to content

Commit f94a82d

Browse files
authored
Add flutter: config: {...} section to pubspec.yaml to influence FeatureFlags (flutter#167953)
Closes flutter#167667. In this PR: - `FlutterFeaturesConfig` reads from the global config, current pubspec.yaml, and environment - `FeatureFlags` now delegates "was this configured" to `FlutterFeaturesConfig` - `featureFlags` (`Context`) now is created with knowledge of the current `pubpec.yaml`, if any Once this lands and we play with it a bit (i.e. migrate `disable-swift-package-manager`), we can publicize it and update website documentation (it won't make it into this beta/stable release anyway). I did a significant rewrite of `feature_test`, as lots of what was being tested no longer made sense.
1 parent 2a3e27f commit f94a82d

File tree

7 files changed

+724
-320
lines changed

7 files changed

+724
-320
lines changed

packages/flutter_tools/lib/src/context_runner.dart

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ import 'flutter_application_package.dart';
4141
import 'flutter_cache.dart';
4242
import 'flutter_device_manager.dart';
4343
import 'flutter_features.dart';
44+
import 'flutter_features_config.dart';
45+
import 'flutter_manifest.dart';
4446
import 'globals.dart' as globals;
4547
import 'ios/ios_workflow.dart';
4648
import 'ios/iproxy.dart';
@@ -233,7 +235,15 @@ Future<T> runInContext<T>(FutureOr<T> Function() runner, {Map<Type, Generator>?
233235
FeatureFlags:
234236
() => FlutterFeatureFlags(
235237
flutterVersion: globals.flutterVersion,
236-
config: globals.config,
238+
featuresConfig: FlutterFeaturesConfig(
239+
globalConfig: globals.config,
240+
platform: globals.platform,
241+
projectManifest: FlutterManifest.createFromPath(
242+
globals.fs.path.join(globals.fs.currentDirectory.path, 'pubspec.yaml'),
243+
fileSystem: globals.fs,
244+
logger: globals.logger,
245+
),
246+
),
237247
platform: globals.platform,
238248
),
239249
FlutterVersion: () => FlutterVersion(fs: globals.fs, flutterRoot: Cache.flutterRoot!),

packages/flutter_tools/lib/src/features.dart

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'package:meta/meta.dart';
6+
57
import 'base/context.dart';
68

79
/// The current [FeatureFlags] implementation.
@@ -275,7 +277,8 @@ class Feature {
275277
}
276278

277279
/// A description of the conditions to enable a feature for a particular channel.
278-
class FeatureChannelSetting {
280+
@immutable
281+
final class FeatureChannelSetting {
279282
const FeatureChannelSetting({this.available = false, this.enabledByDefault = false});
280283

281284
/// Whether the feature is available on this channel.
@@ -288,4 +291,19 @@ class FeatureChannelSetting {
288291
///
289292
/// If not provided, defaults to `false`.
290293
final bool enabledByDefault;
294+
295+
@override
296+
bool operator ==(Object other) {
297+
return other is FeatureChannelSetting &&
298+
available == other.available &&
299+
enabledByDefault == other.enabledByDefault;
300+
}
301+
302+
@override
303+
int get hashCode => Object.hash(available, enabledByDefault);
304+
305+
@override
306+
String toString() {
307+
return 'FeatureChannelSetting <available: $available, enabledByDefault: $enabledByDefault>';
308+
}
291309
}

packages/flutter_tools/lib/src/flutter_features.dart

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,17 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import 'base/config.dart';
5+
import 'package:meta/meta.dart';
6+
67
import 'base/platform.dart';
78
import 'features.dart';
9+
import 'flutter_features_config.dart';
810
import 'version.dart';
911

10-
class FlutterFeatureFlags implements FeatureFlags {
11-
FlutterFeatureFlags({
12-
required FlutterVersion flutterVersion,
13-
required Config config,
14-
required Platform platform,
15-
}) : _flutterVersion = flutterVersion,
16-
_config = config,
17-
_platform = platform;
18-
19-
final FlutterVersion _flutterVersion;
20-
final Config _config;
21-
final Platform _platform;
12+
@visibleForTesting
13+
mixin FlutterFeatureFlagsIsEnabled implements FeatureFlags {
14+
@protected
15+
Platform get platform;
2216

2317
@override
2418
bool get isLinuxEnabled => isEnabled(flutterLinuxDesktopFeature);
@@ -46,7 +40,7 @@ class FlutterFeatureFlags implements FeatureFlags {
4640

4741
@override
4842
bool get isCliAnimationEnabled {
49-
if (_platform.environment['TERM'] == 'dumb') {
43+
if (platform.environment['TERM'] == 'dumb') {
5044
return false;
5145
}
5246
return isEnabled(cliAnimation);
@@ -60,26 +54,34 @@ class FlutterFeatureFlags implements FeatureFlags {
6054

6155
@override
6256
bool get isExplicitPackageDependenciesEnabled => isEnabled(explicitPackageDependencies);
57+
}
58+
59+
interface class FlutterFeatureFlags with FlutterFeatureFlagsIsEnabled implements FeatureFlags {
60+
FlutterFeatureFlags({
61+
required FlutterVersion flutterVersion,
62+
required FlutterFeaturesConfig featuresConfig,
63+
required this.platform,
64+
}) : _flutterVersion = flutterVersion,
65+
_featuresConfig = featuresConfig;
66+
67+
final FlutterVersion _flutterVersion;
68+
final FlutterFeaturesConfig _featuresConfig;
69+
70+
@override
71+
@protected
72+
final Platform platform;
6373

6474
@override
6575
bool isEnabled(Feature feature) {
6676
final String currentChannel = _flutterVersion.channel;
6777
final FeatureChannelSetting featureSetting = feature.getSettingForChannel(currentChannel);
78+
79+
// If unavailable, then no setting can enable this feature.
6880
if (!featureSetting.available) {
6981
return false;
7082
}
71-
bool isEnabled = featureSetting.enabledByDefault;
72-
if (feature.configSetting != null) {
73-
final bool? configOverride = _config.getValue(feature.configSetting!) as bool?;
74-
if (configOverride != null) {
75-
isEnabled = configOverride;
76-
}
77-
}
78-
if (feature.environmentOverride != null) {
79-
if (_platform.environment[feature.environmentOverride]?.toLowerCase() == 'true') {
80-
isEnabled = true;
81-
}
82-
}
83-
return isEnabled;
83+
84+
// Otherwise, read it from environment variable > project manifest > global config
85+
return _featuresConfig.isEnabled(feature) ?? featureSetting.enabledByDefault;
8486
}
8587
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'base/common.dart';
6+
import 'base/config.dart';
7+
import 'base/platform.dart';
8+
import 'features.dart';
9+
import 'flutter_manifest.dart';
10+
11+
/// Reads configuration flags to possibly override the default flag value.
12+
///
13+
/// See [isEnabled] for details on how feature flag values are resolved.
14+
interface class FlutterFeaturesConfig {
15+
/// Creates a feature configuration reader from the provided sources.
16+
///
17+
/// [globalConfig] reads values stored by the `flutter config` tool, which
18+
/// are normally in the user's `%HOME` directory (varies by system), while
19+
/// [projectManifest] reads values from the _current_ Flutter project's
20+
/// `pubspec.yaml`
21+
const FlutterFeaturesConfig({
22+
required Config globalConfig,
23+
required Platform platform,
24+
required FlutterManifest? projectManifest,
25+
}) : _globalConfig = globalConfig,
26+
_platform = platform,
27+
_projectManifest = projectManifest;
28+
29+
final Config _globalConfig;
30+
final Platform _platform;
31+
32+
// Can be null if no manifest file exists in the current directory.
33+
final FlutterManifest? _projectManifest;
34+
35+
/// Returns whether [feature] has been turned on/off from configuration.
36+
///
37+
/// If the feature was not configured, or cannot be configured, returns `null`.
38+
///
39+
/// The value is resolved, if possible, in the following order, where if a
40+
/// step resolves to a boolean value, no further steps are attempted:
41+
///
42+
///
43+
/// ## 1. Local Project Configuration
44+
///
45+
/// If [Feature.configSetting] is `null`, this step is skipped.
46+
///
47+
/// If the value defined by the key `$configSetting` is set in `pubspec.yaml`,
48+
/// it is returned as a boolean value.
49+
///
50+
/// Assuming there is a setting where `configSetting: 'enable-foo'`:
51+
///
52+
/// ```yaml
53+
/// # true
54+
/// flutter:
55+
/// config:
56+
/// enable-foo: true
57+
///
58+
/// # false
59+
/// flutter:
60+
/// config:
61+
/// enable-foo: false
62+
/// ```
63+
///
64+
/// ## 2. Global Tool Configuration
65+
///
66+
/// If [Feature.configSetting] is `null`, this step is skipped.
67+
///
68+
/// If the value defined by the key `$configSetting` is set in the global
69+
/// (platform dependent) configuration file, it is returned as a boolean
70+
/// value.
71+
///
72+
/// Assuming there is a setting where `configSetting: 'enable-foo'`:
73+
///
74+
/// ```sh
75+
/// # future runs will treat the value as true
76+
/// flutter config --enable-foo
77+
///
78+
/// # future runs will treat the value as false
79+
/// flutter config --no-enable-foo
80+
/// ```
81+
///
82+
/// ## 3. Environment Variable
83+
///
84+
/// If [Feature.environmentOverride] is `null`, this step is skipped.
85+
///
86+
/// If the value defined by the key `$environmentOverride` is equal to the
87+
/// string `'true'` (case insensitive), returns `true`, or `false` otherwise.
88+
///
89+
/// Assuming there is a flag where `environmentOverride: 'ENABLE_FOO'`:
90+
///
91+
/// ```sh
92+
/// # true
93+
/// ENABLE_FOO=true flutter some-command
94+
///
95+
/// # true
96+
/// ENABLE_FOO=TRUE flutter some-command
97+
///
98+
/// # false
99+
/// ENABLE_FOO=false flutter some-command
100+
///
101+
/// # false
102+
/// ENABLE_FOO=any-other-value flutter some-command
103+
/// ```
104+
bool? isEnabled(Feature feature) {
105+
return _isEnabledByConfigValue(feature) ?? _isEnabledByPlatformEnvironment(feature);
106+
}
107+
108+
bool? _isEnabledByConfigValue(Feature feature) {
109+
// If the feature cannot be configured by local/global config settings, return null.
110+
final String? featureName = feature.configSetting;
111+
if (featureName == null) {
112+
return null;
113+
}
114+
return _isEnabledAtProjectLevel(featureName) ?? _isEnabledByGlobalConfig(featureName);
115+
}
116+
117+
bool? _isEnabledByPlatformEnvironment(Feature feature) {
118+
// If the feature cannot be configured by an environment variable, return null.
119+
final String? environmentName = feature.environmentOverride;
120+
if (environmentName == null) {
121+
return null;
122+
}
123+
final Object? environmentValue = _platform.environment[environmentName]?.toLowerCase();
124+
if (environmentValue == null) {
125+
return null;
126+
}
127+
return environmentValue == 'true';
128+
}
129+
130+
bool? _isEnabledAtProjectLevel(String featureName) {
131+
final Object? configSection = _projectManifest?.flutterDescriptor['config'];
132+
if (configSection == null) {
133+
return null;
134+
}
135+
if (configSection is! Map) {
136+
throwToolExit(
137+
'The "config" property of "flutter" in pubspec.yaml must be a map, but '
138+
'got $configSection (${configSection.runtimeType})',
139+
);
140+
}
141+
return _requireBoolOrNull(
142+
configSection[featureName],
143+
featureName: featureName,
144+
source: '"flutter: config:" in pubspec.yaml',
145+
);
146+
}
147+
148+
bool? _isEnabledByGlobalConfig(String featureName) {
149+
return _requireBoolOrNull(
150+
_globalConfig.getValue(featureName),
151+
featureName: featureName,
152+
source: '"${_globalConfig.configPath}"',
153+
);
154+
}
155+
156+
static bool? _requireBoolOrNull(
157+
Object? value, {
158+
required String featureName,
159+
required String source,
160+
}) {
161+
if (value is bool?) {
162+
return value;
163+
}
164+
throwToolExit(
165+
'The "$featureName" property in $source must be a boolean, but got $value (${value.runtimeType})',
166+
);
167+
}
168+
}

packages/flutter_tools/lib/src/flutter_manifest.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,9 @@ void _validateFlutter(YamlMap? yaml, List<String> errors) {
608608
errors.addAll(pluginErrors);
609609
case 'generate':
610610
break;
611+
case 'config':
612+
// Futher validation is defined in FlutterFeaturesConfig.
613+
break;
611614
case 'deferred-components':
612615
_validateDeferredComponents(kvp, errors);
613616
case 'disable-swift-package-manager':

0 commit comments

Comments
 (0)