Reland "Added --dart-flags option to flutter run (#33924)" (#34181)
Reland "Added --dart-flags option to flutter run (#33924)"
This reverts commit 587687eedf81aa83521cca96b8fb65a4adc8dd90.
diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart
index c9c5c71..727e25b 100644
--- a/packages/flutter_tools/lib/src/android/android_device.dart
+++ b/packages/flutter_tools/lib/src/android/android_device.dart
@@ -491,6 +491,8 @@
cmd.addAll(<String>['--ez', 'start-paused', 'true']);
if (debuggingOptions.disableServiceAuthCodes)
cmd.addAll(<String>['--ez', 'disable-service-auth-codes', 'true']);
+ if (debuggingOptions.dartFlags.isNotEmpty)
+ cmd.addAll(<String>['--es', 'dart-flags', debuggingOptions.dartFlags]);
if (debuggingOptions.useTestFonts)
cmd.addAll(<String>['--ez', 'use-test-fonts', 'true']);
if (debuggingOptions.verboseSystemLogs) {
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index 1763364..3fb4bf8 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -4,6 +4,8 @@
import 'dart:async';
+import 'package:args/command_runner.dart';
+
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/time.dart';
@@ -130,6 +132,16 @@
defaultsTo: true,
help: 'If necessary, build the app before running.',
)
+ ..addOption('dart-flags',
+ hide: !verboseHelp,
+ help: 'Pass a list of comma separated flags to the Dart instance at '
+ 'application startup. Flags passed through this option must be '
+ 'present on the whitelist defined within the Flutter engine. If '
+ 'a non-whitelisted flag is encountered, the process will be '
+ 'terminated immediately.\n\n'
+ 'This flag is not available on the stable channel and is only '
+ 'applied in debug and profile modes. This option should only '
+ 'be used for experiments and should not be used by typical users.')
..addOption('use-application-binary',
hide: !verboseHelp,
help: 'Specify a pre-built application binary to use when running.',
@@ -294,6 +306,7 @@
buildInfo,
startPaused: argResults['start-paused'],
disableServiceAuthCodes: argResults['disable-service-auth-codes'],
+ dartFlags: argResults['dart-flags'] ?? '',
useTestFonts: argResults['use-test-fonts'],
enableSoftwareRendering: argResults['enable-software-rendering'],
skiaDeterministicRendering: argResults['skia-deterministic-rendering'],
@@ -350,6 +363,11 @@
);
}
+ if (argResults['dart-flags'] != null && FlutterVersion.instance.isStable) {
+ throw UsageException('--dart-flags is not available on the stable '
+ 'channel.', null);
+ }
+
for (Device device in devices) {
if (await device.isLocalEmulator) {
if (await device.supportsHardwareRendering) {
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index 394befb..3947ce3 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -377,6 +377,7 @@
this.buildInfo, {
this.startPaused = false,
this.disableServiceAuthCodes = false,
+ this.dartFlags = '',
this.enableSoftwareRendering = false,
this.skiaDeterministicRendering = false,
this.traceSkia = false,
@@ -391,6 +392,7 @@
: debuggingEnabled = false,
useTestFonts = false,
startPaused = false,
+ dartFlags = '',
disableServiceAuthCodes = false,
enableSoftwareRendering = false,
skiaDeterministicRendering = false,
@@ -404,6 +406,7 @@
final BuildInfo buildInfo;
final bool startPaused;
+ final String dartFlags;
final bool disableServiceAuthCodes;
final bool enableSoftwareRendering;
final bool skiaDeterministicRendering;
diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart
index 02ede71..15f2a2e 100644
--- a/packages/flutter_tools/lib/src/ios/devices.dart
+++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -283,6 +283,11 @@
if (debuggingOptions.disableServiceAuthCodes)
launchArguments.add('--disable-service-auth-codes');
+ if (debuggingOptions.dartFlags.isNotEmpty) {
+ final String dartFlags = debuggingOptions.dartFlags;
+ launchArguments.add('--dart-flags="$dartFlags"');
+ }
+
if (debuggingOptions.useTestFonts)
launchArguments.add('--use-test-fonts');
diff --git a/packages/flutter_tools/test/commands/run_test.dart b/packages/flutter_tools/test/commands/run_test.dart
index 492c149..b425238 100644
--- a/packages/flutter_tools/test/commands/run_test.dart
+++ b/packages/flutter_tools/test/commands/run_test.dart
@@ -2,11 +2,15 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'package:args/command_runner.dart';
+import 'package:flutter_tools/src/application_package.dart';
import 'package:flutter_tools/src/base/common.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/runner/flutter_command.dart';
+import 'package:flutter_tools/src/version.dart';
import 'package:mockito/mockito.dart';
import '../src/common.dart';
@@ -14,9 +18,20 @@
import '../src/mocks.dart';
void main() {
- final MockDeviceManager mockDeviceManager = MockDeviceManager();
-
group('run', () {
+ MockApplicationPackageFactory mockApplicationPackageFactory;
+ MockDeviceManager mockDeviceManager;
+ MockFlutterVersion mockStableFlutterVersion;
+ MockFlutterVersion mockUnstableFlutterVersion;
+
+ setUpAll(() {
+ Cache.disableLocking();
+ mockApplicationPackageFactory = MockApplicationPackageFactory();
+ mockDeviceManager = MockDeviceManager();
+ mockStableFlutterVersion = MockFlutterVersion(isStable: true);
+ mockUnstableFlutterVersion = MockFlutterVersion(isStable: false);
+ });
+
testUsingContext('fails when target not found', () async {
final RunCommand command = RunCommand();
applyMocksToCommand(command);
@@ -28,6 +43,92 @@
}
});
+
+ group('dart-flags option', () {
+ setUpAll(() {
+ when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) {
+ return Stream<Device>.fromIterable(<Device>[
+ FakeDevice(),
+ ]);
+ });
+ });
+
+ RunCommand command;
+ List<String> args;
+ setUp(() {
+ command = TestRunCommand();
+ args = <String> [
+ 'run',
+ '--dart-flags', '"--observe"',
+ '--no-hot',
+ ];
+ });
+
+ testUsingContext('is not available on stable channel', () async {
+ // Stable branch.
+ try {
+ await createTestCommandRunner(command).run(args);
+ fail('Expect exception');
+ // ignore: unused_catch_clause
+ } on UsageException catch(e) {
+ // Not available while on stable branch.
+ }
+ }, overrides: <Type, Generator>{
+ DeviceManager: () => mockDeviceManager,
+ FlutterVersion: () => mockStableFlutterVersion,
+ });
+
+ testUsingContext('is populated in debug mode', () async {
+ // FakeDevice.startApp checks that --dart-flags doesn't get dropped and
+ // throws ToolExit with FakeDevice.kSuccess if the flag is populated.
+ try {
+ await createTestCommandRunner(command).run(args);
+ fail('Expect exception');
+ } on ToolExit catch (e) {
+ expect(e.exitCode, FakeDevice.kSuccess);
+ }
+ }, overrides: <Type, Generator>{
+ ApplicationPackageFactory: () => mockApplicationPackageFactory,
+ DeviceManager: () => mockDeviceManager,
+ FlutterVersion: () => mockUnstableFlutterVersion,
+ });
+
+ testUsingContext('is populated in profile mode', () async {
+ args.add('--profile');
+
+ // FakeDevice.startApp checks that --dart-flags doesn't get dropped and
+ // throws ToolExit with FakeDevice.kSuccess if the flag is populated.
+ try {
+ await createTestCommandRunner(command).run(args);
+ fail('Expect exception');
+ } on ToolExit catch (e) {
+ expect(e.exitCode, FakeDevice.kSuccess);
+ }
+ }, overrides: <Type, Generator>{
+ ApplicationPackageFactory: () => mockApplicationPackageFactory,
+ DeviceManager: () => mockDeviceManager,
+ FlutterVersion: () => mockUnstableFlutterVersion,
+ });
+
+ testUsingContext('is not populated in release mode', () async {
+ args.add('--release');
+
+ // FakeDevice.startApp checks that --dart-flags *does* get dropped and
+ // throws ToolExit with FakeDevice.kSuccess if the flag is set to the
+ // empty string.
+ try {
+ await createTestCommandRunner(command).run(args);
+ fail('Expect exception');
+ } on ToolExit catch (e) {
+ expect(e.exitCode, FakeDevice.kSuccess);
+ }
+ }, overrides: <Type, Generator>{
+ ApplicationPackageFactory: () => mockApplicationPackageFactory,
+ DeviceManager: () => mockDeviceManager,
+ FlutterVersion: () => mockUnstableFlutterVersion,
+ });
+ });
+
testUsingContext('should only request artifacts corresponding to connected devices', () async {
when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) {
return Stream<Device>.fromIterable(<Device>[
@@ -89,3 +190,73 @@
@override
Future<TargetPlatform> get targetPlatform async => _targetPlatform;
}
+
+class TestRunCommand extends RunCommand {
+ @override
+ // ignore: must_call_super
+ Future<void> validateCommand() async {
+ devices = await deviceManager.getDevices().toList();
+ }
+}
+
+class MockStableFlutterVersion extends MockFlutterVersion {
+ @override
+ bool get isStable => true;
+}
+
+class FakeDevice extends Fake implements Device {
+ static const int kSuccess = 1;
+ static const int kFailure = -1;
+ final TargetPlatform _targetPlatform = TargetPlatform.ios;
+
+ void _throwToolExit(int code) => throwToolExit(null, exitCode: code);
+
+ @override
+ Future<bool> get isLocalEmulator => Future<bool>.value(false);
+
+ @override
+ bool get supportsHotReload => false;
+
+ @override
+ Future<String> get sdkNameAndVersion => Future<String>.value('');
+
+ @override
+ DeviceLogReader getLogReader({ ApplicationPackage app }) {
+ return MockDeviceLogReader();
+ }
+
+ @override
+ String get name => 'FakeDevice';
+
+ @override
+ Future<TargetPlatform> get targetPlatform async => _targetPlatform;
+
+ @override
+ Future<LaunchResult> startApp(
+ ApplicationPackage package, {
+ String mainPath,
+ String route,
+ DebuggingOptions debuggingOptions,
+ Map<String, dynamic> platformArgs,
+ bool prebuiltApplication = false,
+ bool usesTerminalUi = true,
+ bool ipv6 = false,
+ }) async {
+ final String dartFlags = debuggingOptions.dartFlags;
+ // In release mode, --dart-flags should be set to the empty string and
+ // provided flags should be dropped. In debug and profile modes,
+ // --dart-flags should not be empty.
+ if (debuggingOptions.buildInfo.isRelease) {
+ if (dartFlags.isNotEmpty) {
+ _throwToolExit(kFailure);
+ }
+ _throwToolExit(kSuccess);
+ } else {
+ if (dartFlags.isEmpty) {
+ _throwToolExit(kFailure);
+ }
+ _throwToolExit(kSuccess);
+ }
+ return null;
+ }
+}
diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart
index 263437e..cc145f3 100644
--- a/packages/flutter_tools/test/src/context.dart
+++ b/packages/flutter_tools/test/src/context.dart
@@ -332,8 +332,12 @@
}
class MockFlutterVersion extends Mock implements FlutterVersion {
+ MockFlutterVersion({bool isStable = false}) : _isStable = isStable;
+
+ final bool _isStable;
+
@override
- bool get isStable => false;
+ bool get isStable => _isStable;
}
class MockClock extends Mock implements SystemClock {}
diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart
index 5e93576..aad8665 100644
--- a/packages/flutter_tools/test/src/mocks.dart
+++ b/packages/flutter_tools/test/src/mocks.dart
@@ -37,6 +37,18 @@
);
}
+class MockApplicationPackageFactory extends Mock implements ApplicationPackageFactory {
+ final MockApplicationPackageStore _store = MockApplicationPackageStore();
+
+ @override
+ Future<ApplicationPackage> getPackageForPlatform(
+ TargetPlatform platform, {
+ File applicationBinary,
+ }) async {
+ return _store.getPackageForPlatform(platform);
+ }
+}
+
/// An SDK installation with several SDK levels (19, 22, 23).
class MockAndroidSdk extends Mock implements AndroidSdk {
static Directory createSdkDirectory({