Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
36c37cca
Unverified
Commit
36c37cca
authored
Jul 17, 2019
by
Jonah Williams
Committed by
GitHub
Jul 17, 2019
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Implement feature flag system for flutter tools (#36138)
parent
3a60713c
Changes
8
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
814 additions
and
9 deletions
+814
-9
config.dart
packages/flutter_tools/lib/src/base/config.dart
+1
-1
config.dart
packages/flutter_tools/lib/src/commands/config.dart
+57
-7
context_runner.dart
packages/flutter_tools/lib/src/context_runner.dart
+2
-0
features.dart
packages/flutter_tools/lib/src/features.dart
+220
-0
usage.dart
packages/flutter_tools/lib/src/reporting/usage.dart
+16
-1
config_test.dart
...lutter_tools/test/general.shard/commands/config_test.dart
+83
-0
config_test.dart
packages/flutter_tools/test/general.shard/config_test.dart
+7
-0
features_test.dart
packages/flutter_tools/test/general.shard/features_test.dart
+428
-0
No files found.
packages/flutter_tools/lib/src/base/config.dart
View file @
36c37cca
...
...
@@ -27,7 +27,7 @@ class Config {
dynamic
getValue
(
String
key
)
=>
_values
[
key
];
void
setValue
(
String
key
,
String
value
)
{
void
setValue
(
String
key
,
Object
value
)
{
_values
[
key
]
=
value
;
_flushValues
();
}
...
...
packages/flutter_tools/lib/src/commands/config.dart
View file @
36c37cca
...
...
@@ -7,9 +7,11 @@ import 'dart:async';
import
'../android/android_sdk.dart'
;
import
'../android/android_studio.dart'
;
import
'../convert.dart'
;
import
'../features.dart'
;
import
'../globals.dart'
;
import
'../reporting/usage.dart'
;
import
'../runner/flutter_command.dart'
;
import
'../version.dart'
;
class
ConfigCommand
extends
FlutterCommand
{
ConfigCommand
({
bool
verboseHelp
=
false
})
{
...
...
@@ -26,6 +28,21 @@ class ConfigCommand extends FlutterCommand {
negatable:
false
,
hide:
!
verboseHelp
,
help:
'Print config values as json.'
);
for
(
Feature
feature
in
allFeatures
)
{
if
(
feature
.
configSetting
==
null
)
{
continue
;
}
argParser
.
addFlag
(
feature
.
configSetting
,
help:
feature
.
generateHelpMessage
(),
negatable:
true
,
);
}
argParser
.
addFlag
(
'clear-features'
,
help:
'Remove all configured features and restore them to the default values.'
,
negatable:
false
,
);
}
@override
...
...
@@ -44,15 +61,28 @@ class ConfigCommand extends FlutterCommand {
@override
bool
get
shouldUpdateCache
=>
false
;
@override
Future
<
Set
<
DevelopmentArtifact
>>
get
requiredArtifacts
async
=>
const
<
DevelopmentArtifact
>{};
@override
String
get
usageFooter
{
// List all config settings.
String
values
=
config
.
keys
.
map
<
String
>((
String
key
)
{
return
'
$key
:
${config.getValue(key)}
'
;
}).
join
(
'
\n
'
);
// List all config settings. for feature flags, include whether they
// are available.
final
Map
<
String
,
Feature
>
featuresByName
=
<
String
,
Feature
>{};
final
String
channel
=
FlutterVersion
.
instance
.
channel
;
for
(
Feature
feature
in
allFeatures
)
{
if
(
feature
.
configSetting
!=
null
)
{
featuresByName
[
feature
.
configSetting
]
=
feature
;
}
}
String
values
=
config
.
keys
.
map
<
String
>((
String
key
)
{
String
configFooter
=
''
;
if
(
featuresByName
.
containsKey
(
key
))
{
final
FeatureChannelSetting
setting
=
featuresByName
[
key
].
getSettingForChannel
(
channel
);
if
(!
setting
.
available
)
{
configFooter
=
'(Unavailable)'
;
}
}
return
'
$key
:
${config.getValue(key)}
$configFooter
'
;
}).
join
(
'
\n
'
);
if
(
values
.
isEmpty
)
values
=
' No settings have been configured.'
;
return
...
...
@@ -71,6 +101,15 @@ class ConfigCommand extends FlutterCommand {
return
null
;
}
if
(
argResults
[
'clear-features'
])
{
for
(
Feature
feature
in
allFeatures
)
{
if
(
feature
.
configSetting
!=
null
)
{
config
.
removeValue
(
feature
.
configSetting
);
}
}
return
null
;
}
if
(
argResults
.
wasParsed
(
'analytics'
))
{
final
bool
value
=
argResults
[
'analytics'
];
flutterUsage
.
enabled
=
value
;
...
...
@@ -89,6 +128,17 @@ class ConfigCommand extends FlutterCommand {
if
(
argResults
.
wasParsed
(
'clear-ios-signing-cert'
))
_updateConfig
(
'ios-signing-cert'
,
''
);
for
(
Feature
feature
in
allFeatures
)
{
if
(
feature
.
configSetting
==
null
)
{
continue
;
}
if
(
argResults
.
wasParsed
(
feature
.
configSetting
))
{
final
bool
keyValue
=
argResults
[
feature
.
configSetting
];
config
.
setValue
(
feature
.
configSetting
,
keyValue
);
printStatus
(
'Setting "
${feature.configSetting}
" value to "
$keyValue
".'
);
}
}
if
(
argResults
.
arguments
.
isEmpty
)
printStatus
(
usage
);
...
...
packages/flutter_tools/lib/src/context_runner.dart
View file @
36c37cca
...
...
@@ -28,6 +28,7 @@ import 'devfs.dart';
import
'device.dart'
;
import
'doctor.dart'
;
import
'emulator.dart'
;
import
'features.dart'
;
import
'fuchsia/fuchsia_device.dart'
show
FuchsiaDeviceTools
;
import
'fuchsia/fuchsia_sdk.dart'
show
FuchsiaSdk
,
FuchsiaArtifacts
;
import
'fuchsia/fuchsia_workflow.dart'
show
FuchsiaWorkflow
;
...
...
@@ -79,6 +80,7 @@ Future<T> runInContext<T>(
Doctor:
()
=>
const
Doctor
(),
DoctorValidatorsProvider:
()
=>
DoctorValidatorsProvider
.
defaultInstance
,
EmulatorManager:
()
=>
EmulatorManager
(),
FeatureFlags:
()
=>
const
FeatureFlags
(),
Flags:
()
=>
const
EmptyFlags
(),
FlutterVersion:
()
=>
FlutterVersion
(
const
SystemClock
()),
FuchsiaArtifacts:
()
=>
FuchsiaArtifacts
.
find
(),
...
...
packages/flutter_tools/lib/src/features.dart
0 → 100644
View file @
36c37cca
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'package:meta/meta.dart'
;
import
'base/config.dart'
;
import
'base/context.dart'
;
import
'base/platform.dart'
;
import
'version.dart'
;
/// The current [FeatureFlags] implementation.
///
/// If not injected, a default implementation is provided.
FeatureFlags
get
featureFlags
=>
context
.
get
<
FeatureFlags
>();
/// The interface used to determine if a particular [Feature] is enabled.
///
/// The rest of the tools code should use this class instead of looking up
/// features directly. To faciliate rolls to google3 and other clients, all
/// flags should be provided with a default implementation here. Clients that
/// use this class should extent instead of implement, so that new flags are
/// picked up automatically.
class
FeatureFlags
{
const
FeatureFlags
();
/// Whether flutter desktop for linux is enabled.
bool
get
isLinuxEnabled
=>
_isEnabled
(
flutterLinuxDesktopFeature
);
/// Whether flutter desktop for macOS is enabled.
bool
get
isMacOSEnabled
=>
_isEnabled
(
flutterMacOSDesktopFeature
);
/// Whether flutter web is enabled.
bool
get
isWebEnabled
=>
_isEnabled
(
flutterWebFeature
);
/// Whether flutter desktop for Windows is enabled.
bool
get
isWindowsEnabled
=>
_isEnabled
(
flutterWindowsDesktopFeature
);
// Calculate whether a particular feature is enabled for the current channel.
static
bool
_isEnabled
(
Feature
feature
)
{
final
String
currentChannel
=
FlutterVersion
.
instance
.
channel
;
final
FeatureChannelSetting
featureSetting
=
feature
.
getSettingForChannel
(
currentChannel
);
if
(!
featureSetting
.
available
)
{
return
false
;
}
bool
isEnabled
=
featureSetting
.
enabledByDefault
;
if
(
feature
.
configSetting
!=
null
)
{
final
bool
configOverride
=
Config
.
instance
.
getValue
(
feature
.
configSetting
);
if
(
configOverride
!=
null
)
{
isEnabled
=
configOverride
;
}
}
if
(
feature
.
environmentOverride
!=
null
)
{
if
(
platform
.
environment
[
feature
.
environmentOverride
]?.
toLowerCase
()
==
'true'
)
{
isEnabled
=
true
;
}
}
return
isEnabled
;
}
}
/// All current Flutter feature flags.
const
List
<
Feature
>
allFeatures
=
<
Feature
>[
flutterWebFeature
,
flutterLinuxDesktopFeature
,
flutterMacOSDesktopFeature
,
flutterWindowsDesktopFeature
,
];
/// The [Feature] for flutter web.
const
Feature
flutterWebFeature
=
Feature
(
name:
'Flutter Web'
,
configSetting:
'enable-web'
,
environmentOverride:
'FLUTTER_WEB'
,
master:
FeatureChannelSetting
(
available:
true
,
enabledByDefault:
false
,
),
dev:
FeatureChannelSetting
(
available:
true
,
enabledByDefault:
false
,
),
);
/// The [Feature] for macOS desktop.
const
Feature
flutterMacOSDesktopFeature
=
Feature
(
name:
'Flutter Desktop for macOS'
,
configSetting:
'enable-macos-desktop'
,
environmentOverride:
'ENABLE_FLUTTER_DESKTOP'
,
master:
FeatureChannelSetting
(
available:
true
,
enabledByDefault:
false
,
),
);
/// The [Feature] for Linux desktop.
const
Feature
flutterLinuxDesktopFeature
=
Feature
(
name:
'Flutter Desktop for Linux'
,
configSetting:
'enable-linux-desktop'
,
environmentOverride:
'ENABLE_FLUTTER_DESKTOP'
,
master:
FeatureChannelSetting
(
available:
true
,
enabledByDefault:
false
,
),
);
/// The [Feature] for Windows desktop.
const
Feature
flutterWindowsDesktopFeature
=
Feature
(
name:
'Flutter Desktop for Windows'
,
configSetting:
'enable-windows-desktop'
,
environmentOverride:
'ENABLE_FLUTTER_DESKTOP'
,
master:
FeatureChannelSetting
(
available:
true
,
enabledByDefault:
false
,
),
);
/// A [Feature] is a process for conditionally enabling tool features.
///
/// All settings are optional, and if not provided will generally default to
/// a "safe" value, such as being off.
///
/// The top level feature settings can be provided to apply to all channels.
/// Otherwise, more specific settings take precidence over higher level
/// settings.
class
Feature
{
/// Creates a [Feature].
const
Feature
({
@required
this
.
name
,
this
.
environmentOverride
,
this
.
configSetting
,
this
.
master
=
const
FeatureChannelSetting
(),
this
.
dev
=
const
FeatureChannelSetting
(),
this
.
beta
=
const
FeatureChannelSetting
(),
this
.
stable
=
const
FeatureChannelSetting
(),
});
/// The user visible name for this feature.
final
String
name
;
/// The settings for the master branch and other unknown channels.
final
FeatureChannelSetting
master
;
/// The settings for the dev branch.
final
FeatureChannelSetting
dev
;
/// The settings for the beta branch.
final
FeatureChannelSetting
beta
;
/// The settings for the stable branch.
final
FeatureChannelSetting
stable
;
/// The name of an environment variable that can override the setting.
///
/// The environment variable needs to be set to the value 'true'. This is
/// only intended for usage by CI and not as an advertised method to enable
/// a feature.
///
/// If not provided, defaults to `null` meaning there is no override.
final
String
environmentOverride
;
/// The name of a setting that can be used to enable this feature.
///
/// If not provided, defaults to `null` meaning there is no config setting.
final
String
configSetting
;
/// A help message for the `flutter config` command, or null if unsupported.
String
generateHelpMessage
()
{
if
(
configSetting
==
null
)
{
return
null
;
}
final
StringBuffer
buffer
=
StringBuffer
(
'Enable or disable
$name
on '
);
final
List
<
String
>
channels
=
<
String
>[
if
(
master
.
available
)
'master'
,
if
(
dev
.
available
)
'dev'
,
if
(
beta
.
available
)
'beta'
,
if
(
stable
.
available
)
'stable'
,
];
if
(
channels
.
length
==
1
)
{
buffer
.
write
(
'the
${channels.single}
channel.'
);
}
else
{
buffer
.
write
(
'
${channels.join(', ')}
channels.'
);
}
return
buffer
.
toString
();
}
/// Retrieve the correct setting for the provided `channel`.
FeatureChannelSetting
getSettingForChannel
(
String
channel
)
{
switch
(
channel
)
{
case
'stable'
:
return
stable
;
case
'beta'
:
return
beta
;
case
'dev'
:
return
dev
;
case
'master'
:
default
:
return
master
;
}
}
}
/// A description of the conditions to enable a feature for a particular channel.
class
FeatureChannelSetting
{
const
FeatureChannelSetting
({
this
.
available
=
false
,
this
.
enabledByDefault
=
false
,
});
/// Whether the feature is available on this channel.
///
/// If not provded, defaults to `false`. This implies that the feature
/// cannot be enabled even by the settings below.
final
bool
available
;
/// Whether the feature is enabled by default.
///
/// If not provided, defaults to `false`.
final
bool
enabledByDefault
;
}
packages/flutter_tools/lib/src/reporting/usage.dart
View file @
36c37cca
...
...
@@ -7,11 +7,13 @@ import 'dart:async';
import
'package:meta/meta.dart'
;
import
'package:usage/usage_io.dart'
;
import
'../base/config.dart'
;
import
'../base/context.dart'
;
import
'../base/file_system.dart'
;
import
'../base/os.dart'
;
import
'../base/platform.dart'
;
import
'../base/utils.dart'
;
import
'../features.dart'
;
import
'../globals.dart'
;
import
'../version.dart'
;
...
...
@@ -55,7 +57,9 @@ const String reloadExceptionTargetPlatform = 'cd27';
const
String
reloadExceptionSdkName
=
'cd28'
;
const
String
reloadExceptionEmulator
=
'cd29'
;
const
String
reloadExceptionFullRestart
=
'cd30'
;
// Next ID: cd32
const
String
enabledFlutterFeatures
=
'cd32'
;
// Next ID: cd33
Usage
get
flutterUsage
=>
Usage
.
instance
;
...
...
@@ -72,6 +76,17 @@ class Usage {
_analytics
.
setSessionValue
(
kSessionHostOsDetails
,
os
.
name
);
// Send the branch name as the "channel".
_analytics
.
setSessionValue
(
kSessionChannelName
,
flutterVersion
.
getBranchName
(
redactUnknownBranches:
true
));
// For each flutter experimental feature, record a session value in a comma
// separated list.
final
String
enabledFeatures
=
allFeatures
.
where
((
Feature
feature
)
{
return
feature
.
configSetting
!=
null
&&
Config
.
instance
.
getValue
(
feature
.
configSetting
)
==
true
;
})
.
map
((
Feature
feature
)
=>
feature
.
configSetting
)
.
join
(
','
);
_analytics
.
setSessionValue
(
enabledFlutterFeatures
,
enabledFeatures
);
// Record the host as the application installer ID - the context that flutter_tools is running in.
if
(
platform
.
environment
.
containsKey
(
'FLUTTER_HOST'
))
{
_analytics
.
setSessionValue
(
'aiid'
,
platform
.
environment
[
'FLUTTER_HOST'
]);
...
...
packages/flutter_tools/test/general.shard/commands/config_test.dart
View file @
36c37cca
...
...
@@ -4,11 +4,15 @@
import
'dart:convert'
;
import
'package:args/command_runner.dart'
;
import
'package:flutter_tools/src/android/android_sdk.dart'
;
import
'package:flutter_tools/src/android/android_studio.dart'
;
import
'package:flutter_tools/src/base/config.dart'
;
import
'package:flutter_tools/src/base/context.dart'
;
import
'package:flutter_tools/src/base/logger.dart'
;
import
'package:flutter_tools/src/cache.dart'
;
import
'package:flutter_tools/src/commands/config.dart'
;
import
'package:flutter_tools/src/version.dart'
;
import
'package:mockito/mockito.dart'
;
import
'../../src/common.dart'
;
...
...
@@ -17,10 +21,16 @@ import '../../src/context.dart';
void
main
(
)
{
MockAndroidStudio
mockAndroidStudio
;
MockAndroidSdk
mockAndroidSdk
;
MockFlutterVersion
mockFlutterVersion
;
setUpAll
(()
{
Cache
.
disableLocking
();
});
setUp
(()
{
mockAndroidStudio
=
MockAndroidStudio
();
mockAndroidSdk
=
MockAndroidSdk
();
mockFlutterVersion
=
MockFlutterVersion
();
});
group
(
'config'
,
()
{
...
...
@@ -42,6 +52,77 @@ void main() {
AndroidStudio:
()
=>
mockAndroidStudio
,
AndroidSdk:
()
=>
mockAndroidSdk
,
});
testUsingContext
(
'allows setting and removing feature flags'
,
()
async
{
final
ConfigCommand
configCommand
=
ConfigCommand
();
final
CommandRunner
<
void
>
commandRunner
=
createTestCommandRunner
(
configCommand
);
await
commandRunner
.
run
(<
String
>[
'config'
,
'--enable-web'
,
'--enable-linux-desktop'
,
'--enable-windows-desktop'
,
'--enable-macos-desktop'
]);
expect
(
Config
.
instance
.
getValue
(
'enable-web'
),
true
);
expect
(
Config
.
instance
.
getValue
(
'enable-linux-desktop'
),
true
);
expect
(
Config
.
instance
.
getValue
(
'enable-windows-desktop'
),
true
);
expect
(
Config
.
instance
.
getValue
(
'enable-macos-desktop'
),
true
);
await
commandRunner
.
run
(<
String
>[
'config'
,
'--clear-features'
,
]);
expect
(
Config
.
instance
.
getValue
(
'enable-web'
),
null
);
expect
(
Config
.
instance
.
getValue
(
'enable-linux-desktop'
),
null
);
expect
(
Config
.
instance
.
getValue
(
'enable-windows-desktop'
),
null
);
expect
(
Config
.
instance
.
getValue
(
'enable-macos-desktop'
),
null
);
await
commandRunner
.
run
(<
String
>[
'config'
,
'--no-enable-web'
,
'--no-enable-linux-desktop'
,
'--no-enable-windows-desktop'
,
'--no-enable-macos-desktop'
]);
expect
(
Config
.
instance
.
getValue
(
'enable-web'
),
false
);
expect
(
Config
.
instance
.
getValue
(
'enable-linux-desktop'
),
false
);
expect
(
Config
.
instance
.
getValue
(
'enable-windows-desktop'
),
false
);
expect
(
Config
.
instance
.
getValue
(
'enable-macos-desktop'
),
false
);
},
overrides:
<
Type
,
Generator
>{
AndroidStudio:
()
=>
mockAndroidStudio
,
AndroidSdk:
()
=>
mockAndroidSdk
,
});
testUsingContext
(
'displays which config settings are available on stable'
,
()
async
{
final
BufferLogger
logger
=
context
.
get
<
Logger
>();
when
(
mockFlutterVersion
.
channel
).
thenReturn
(
'stable'
);
final
ConfigCommand
configCommand
=
ConfigCommand
();
final
CommandRunner
<
void
>
commandRunner
=
createTestCommandRunner
(
configCommand
);
await
commandRunner
.
run
(<
String
>[
'config'
,
'--enable-web'
,
'--enable-linux-desktop'
,
'--enable-windows-desktop'
,
'--enable-macos-desktop'
]);
await
commandRunner
.
run
(<
String
>[
'config'
,
]);
expect
(
logger
.
statusText
,
contains
(
'enable-web: true (Unavailable)'
));
expect
(
logger
.
statusText
,
contains
(
'enable-linux-desktop: true (Unavailable)'
));
expect
(
logger
.
statusText
,
contains
(
'enable-windows-desktop: true (Unavailable)'
));
expect
(
logger
.
statusText
,
contains
(
'enable-macos-desktop: true (Unavailable)'
));
},
overrides:
<
Type
,
Generator
>{
AndroidStudio:
()
=>
mockAndroidStudio
,
AndroidSdk:
()
=>
mockAndroidSdk
,
FlutterVersion:
()
=>
mockFlutterVersion
,
});
});
}
...
...
@@ -54,3 +135,5 @@ class MockAndroidSdk extends Mock implements AndroidSdk {
@override
String
get
directory
=>
'path/to/android/sdk'
;
}
class
MockFlutterVersion
extends
Mock
implements
FlutterVersion
{}
\ No newline at end of file
packages/flutter_tools/test/general.shard/config_test.dart
View file @
36c37cca
...
...
@@ -29,6 +29,13 @@ void main() {
expect
(
config
.
keys
,
contains
(
'foo'
));
});
test
(
'get set bool value'
,
()
async
{
expect
(
config
.
getValue
(
'foo'
),
null
);
config
.
setValue
(
'foo'
,
true
);
expect
(
config
.
getValue
(
'foo'
),
true
);
expect
(
config
.
keys
,
contains
(
'foo'
));
});
test
(
'containsKey'
,
()
async
{
expect
(
config
.
containsKey
(
'foo'
),
false
);
config
.
setValue
(
'foo'
,
'bar'
);
...
...
packages/flutter_tools/test/general.shard/features_test.dart
0 → 100644
View file @
36c37cca
This diff is collapsed.
Click to expand it.
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment