From 194ea3843b8c838e304ef293a546a462924cf55c Mon Sep 17 00:00:00 2001 From: shindonghwi Date: Tue, 16 Dec 2025 22:14:15 +0900 Subject: [PATCH 1/3] [flutter_tools] Pass --chrome-binary to app hosting Chrome in flutter drive Fixes #171504 The --chrome-binary flag was only being passed to WebDriver Chrome, not to the app hosting Chrome instance. This change passes the flag through DebuggingOptions so both Chrome instances use the specified binary. Changes: - Add webChromeBinary field to DebuggingOptions - Pass chromeBinary parameter to ChromiumLauncher.launch() - Forward webChromeBinary in WebDriverService.start() - Add test for custom chrome binary path --- .../flutter_tools/lib/src/commands/drive.dart | 1 + .../flutter_tools/lib/src/commands/run.dart | 7 +++++- packages/flutter_tools/lib/src/device.dart | 8 +++++++ .../lib/src/drive/web_driver_service.dart | 2 ++ .../flutter_tools/lib/src/web/chrome.dart | 3 ++- .../flutter_tools/lib/src/web/web_device.dart | 1 + .../test/web.shard/chrome_test.dart | 23 +++++++++++++++++++ 7 files changed, 43 insertions(+), 2 deletions(-) diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart index 7f04ed36267..18b060abd07 100644 --- a/packages/flutter_tools/lib/src/commands/drive.dart +++ b/packages/flutter_tools/lib/src/commands/drive.dart @@ -334,6 +334,7 @@ class DriveCommand extends RunCommandBase { final BuildInfo buildInfo = await getBuildInfo(); final DebuggingOptions debuggingOptions = await createDebuggingOptions( webDevServerConfig: webDevServerConfig, + webChromeBinary: stringArg('chrome-binary'), ); final File? applicationBinary = applicationBinaryPath == null ? null diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index 12625e1c814..1b6fb938c14 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -282,7 +282,10 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment /// Create a debugging options instance for the current `run` or `drive` invocation. @visibleForTesting @protected - Future createDebuggingOptions({WebDevServerConfig? webDevServerConfig}) async { + Future createDebuggingOptions({ + WebDevServerConfig? webDevServerConfig, + String? webChromeBinary, + }) async { final BuildInfo buildInfo = await getBuildInfo(); final int? webBrowserDebugPort = featureFlags.isWebEnabled && argResults!.wasParsed('web-browser-debug-port') @@ -311,6 +314,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment webBrowserDebugPort: webBrowserDebugPort, webBrowserFlags: webBrowserFlags, webCrossOriginIsolation: webCrossOriginIsolation, + webChromeBinary: webChromeBinary, webRenderer: webRenderer, webUseWasm: useWasm, enableImpeller: enableImpeller, @@ -364,6 +368,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment webRunHeadless: featureFlags.isWebEnabled && boolArg('web-run-headless'), webBrowserDebugPort: webBrowserDebugPort, webBrowserFlags: webBrowserFlags, + webChromeBinary: webChromeBinary, webEnableExpressionEvaluation: featureFlags.isWebEnabled && boolArg('web-enable-expression-evaluation'), webLaunchUrl: featureFlags.isWebEnabled ? stringArg('web-launch-url') : null, diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index 055c573f0d8..6f5f62263f7 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -985,6 +985,7 @@ class DebuggingOptions { this.webRunHeadless = false, this.webBrowserDebugPort, this.webBrowserFlags = const [], + this.webChromeBinary, this.webEnableExpressionEvaluation = false, this.webLaunchUrl, bool? webCrossOriginIsolation, @@ -1020,6 +1021,7 @@ class DebuggingOptions { this.webRunHeadless = false, this.webBrowserDebugPort, this.webBrowserFlags = const [], + this.webChromeBinary, this.webLaunchUrl, bool? webCrossOriginIsolation, WebRendererMode? webRenderer, @@ -1100,6 +1102,7 @@ class DebuggingOptions { required this.webRunHeadless, required this.webBrowserDebugPort, required this.webBrowserFlags, + required this.webChromeBinary, required this.webEnableExpressionEvaluation, required this.webLaunchUrl, required this.webCrossOriginIsolation, @@ -1185,6 +1188,9 @@ class DebuggingOptions { /// Arbitrary browser flags. final List webBrowserFlags; + /// Custom path to the Chrome executable. + final String? webChromeBinary; + /// Enable expression evaluation for web target. final bool webEnableExpressionEvaluation; @@ -1294,6 +1300,7 @@ class DebuggingOptions { 'webRunHeadless': webRunHeadless, 'webBrowserDebugPort': webBrowserDebugPort, 'webBrowserFlags': webBrowserFlags, + 'webChromeBinary': webChromeBinary, 'webEnableExpressionEvaluation': webEnableExpressionEvaluation, 'webLaunchUrl': webLaunchUrl, 'webCrossOriginIsolation': webCrossOriginIsolation, @@ -1362,6 +1369,7 @@ class DebuggingOptions { webRunHeadless: json['webRunHeadless']! as bool, webBrowserDebugPort: json['webBrowserDebugPort'] as int?, webBrowserFlags: (json['webBrowserFlags']! as List).cast(), + webChromeBinary: json['webChromeBinary'] as String?, webEnableExpressionEvaluation: json['webEnableExpressionEvaluation']! as bool, webLaunchUrl: json['webLaunchUrl'] as String?, webCrossOriginIsolation: json['webCrossOriginIsolation']! as bool, diff --git a/packages/flutter_tools/lib/src/drive/web_driver_service.dart b/packages/flutter_tools/lib/src/drive/web_driver_service.dart index 75e7e088431..1f4091d9d7b 100644 --- a/packages/flutter_tools/lib/src/drive/web_driver_service.dart +++ b/packages/flutter_tools/lib/src/drive/web_driver_service.dart @@ -86,12 +86,14 @@ class WebDriverService extends DriverService { ? DebuggingOptions.disabled( buildInfo, webDevServerConfig: debuggingOptions.webDevServerConfig, + webChromeBinary: debuggingOptions.webChromeBinary, webRenderer: debuggingOptions.webRenderer, webUseWasm: debuggingOptions.webUseWasm, ) : DebuggingOptions.enabled( buildInfo, webDevServerConfig: debuggingOptions.webDevServerConfig, + webChromeBinary: debuggingOptions.webChromeBinary, disablePortPublication: debuggingOptions.disablePortPublication, webRenderer: debuggingOptions.webRenderer, webUseWasm: debuggingOptions.webUseWasm, diff --git a/packages/flutter_tools/lib/src/web/chrome.dart b/packages/flutter_tools/lib/src/web/chrome.dart index 78e1c8a2bd2..077d426aab0 100644 --- a/packages/flutter_tools/lib/src/web/chrome.dart +++ b/packages/flutter_tools/lib/src/web/chrome.dart @@ -189,6 +189,7 @@ class ChromiumLauncher { bool skipCheck = false, Directory? cacheDir, List webBrowserFlags = const [], + String? chromeBinary, }) async { if (currentCompleter.isCompleted) { throwToolExit('Only one instance of chrome can be started.'); @@ -200,7 +201,7 @@ class ChromiumLauncher { ); } - final String chromeExecutable = _browserFinder(_platform, _fileSystem); + final String chromeExecutable = chromeBinary ?? _browserFinder(_platform, _fileSystem); if (_logger.isVerbose) { _logger.printTrace('Will use Chromium executable at $chromeExecutable'); diff --git a/packages/flutter_tools/lib/src/web/web_device.dart b/packages/flutter_tools/lib/src/web/web_device.dart index b664e7f44eb..f987f931b03 100644 --- a/packages/flutter_tools/lib/src/web/web_device.dart +++ b/packages/flutter_tools/lib/src/web/web_device.dart @@ -145,6 +145,7 @@ abstract class ChromiumDevice extends WebDevice { headless: debuggingOptions.webRunHeadless, debugPort: debuggingOptions.webBrowserDebugPort, webBrowserFlags: debuggingOptions.webBrowserFlags, + chromeBinary: debuggingOptions.webChromeBinary, ); } _logger.sendEvent('app.webLaunchUrl', {'url': url, 'launched': launchChrome}); diff --git a/packages/flutter_tools/test/web.shard/chrome_test.dart b/packages/flutter_tools/test/web.shard/chrome_test.dart index c80f0bae3a6..2f9ad4e46ab 100644 --- a/packages/flutter_tools/test/web.shard/chrome_test.dart +++ b/packages/flutter_tools/test/web.shard/chrome_test.dart @@ -527,6 +527,29 @@ void main() { ); }); + testWithoutContext('can launch chrome with custom binary path', () async { + processManager.addCommand( + const FakeCommand( + command: [ + '/custom/path/to/chrome', + '--user-data-dir=/.tmp_rand0/flutter_tools_chrome_device.rand0', + '--remote-debugging-port=12345', + ...kChromeArgs, + 'example_url', + ], + stderr: kDevtoolsStderr, + ), + ); + + await expectReturnsNormallyLater( + chromeLauncher.launch( + 'example_url', + skipCheck: true, + chromeBinary: '/custom/path/to/chrome', + ), + ); + }); + testWithoutContext('can launch chrome headless', () async { processManager.addCommand( const FakeCommand( From b3993b944ed5b9566cbf08686b2106d1ee9a51cf Mon Sep 17 00:00:00 2001 From: shindonghwi Date: Wed, 17 Dec 2025 20:21:42 +0900 Subject: [PATCH 2/3] fix: add chromeBinary parameter to test mock classes Update TestChromiumLauncher.launch() method signature in test files to include the chromeBinary parameter added to ChromiumLauncher.launch() --- .../test/general.shard/resident_web_runner_test.dart | 1 + packages/flutter_tools/test/general.shard/web/devices_test.dart | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart index 618abe6b6c0..5929917f179 100644 --- a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart @@ -2259,6 +2259,7 @@ class TestChromiumLauncher implements ChromiumLauncher { bool skipCheck = false, Directory? cacheDir, List webBrowserFlags = const [], + String? chromeBinary, }) async { return currentCompleter.future; } diff --git a/packages/flutter_tools/test/general.shard/web/devices_test.dart b/packages/flutter_tools/test/general.shard/web/devices_test.dart index 088bcc0a007..a626461d7d7 100644 --- a/packages/flutter_tools/test/general.shard/web/devices_test.dart +++ b/packages/flutter_tools/test/general.shard/web/devices_test.dart @@ -456,6 +456,7 @@ class TestChromiumLauncher implements ChromiumLauncher { bool skipCheck = false, Directory? cacheDir, List webBrowserFlags = const [], + String? chromeBinary, }) async { currentCompleter.complete(_launcher()); return currentCompleter.future; From 2fae58b08f4e0b1ef1e4e6299d4f1a496804f129 Mon Sep 17 00:00:00 2001 From: shindonghwi Date: Sun, 1 Feb 2026 15:37:48 +0900 Subject: [PATCH 3/3] fix: prevent duplicate Chrome launch in flutter drive flutter drive -d chrome was opening Chrome twice - once from WebDriver and once from web_device.dart. Added webNoLaunchChrome flag to DebuggingOptions so we can skip the second launch. --- .../flutter_tools/lib/src/commands/drive.dart | 3 +- .../flutter_tools/lib/src/commands/run.dart | 3 + packages/flutter_tools/lib/src/device.dart | 11 +++ .../lib/src/drive/web_driver_service.dart | 2 + .../flutter_tools/lib/src/web/web_device.dart | 5 +- .../test/general.shard/web/devices_test.dart | 87 +++++++++++++++++++ 6 files changed, 107 insertions(+), 4 deletions(-) diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart index 18b060abd07..ce91d014045 100644 --- a/packages/flutter_tools/lib/src/commands/drive.dart +++ b/packages/flutter_tools/lib/src/commands/drive.dart @@ -332,9 +332,11 @@ class DriveCommand extends RunCommandBase { ); final DriverService driverService = _flutterDriverFactory!.createDriverService(web); final BuildInfo buildInfo = await getBuildInfo(); + final bool isWebDevice = device is ChromiumDevice || device is WebServerDevice; final DebuggingOptions debuggingOptions = await createDebuggingOptions( webDevServerConfig: webDevServerConfig, webChromeBinary: stringArg('chrome-binary'), + webNoLaunchChrome: isWebDevice, ); final File? applicationBinary = applicationBinaryPath == null ? null @@ -353,7 +355,6 @@ class DriveCommand extends RunCommandBase { mainPath: targetFile, platformArgs: { if (traceStartup) 'trace-startup': traceStartup, - if (web) '--no-launch-chrome': true, }, ); } else { diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index 1b6fb938c14..cc828d1a206 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -285,6 +285,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment Future createDebuggingOptions({ WebDevServerConfig? webDevServerConfig, String? webChromeBinary, + bool webNoLaunchChrome = false, }) async { final BuildInfo buildInfo = await getBuildInfo(); final int? webBrowserDebugPort = @@ -315,6 +316,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment webBrowserFlags: webBrowserFlags, webCrossOriginIsolation: webCrossOriginIsolation, webChromeBinary: webChromeBinary, + webNoLaunchChrome: webNoLaunchChrome, webRenderer: webRenderer, webUseWasm: useWasm, enableImpeller: enableImpeller, @@ -369,6 +371,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment webBrowserDebugPort: webBrowserDebugPort, webBrowserFlags: webBrowserFlags, webChromeBinary: webChromeBinary, + webNoLaunchChrome: webNoLaunchChrome, webEnableExpressionEvaluation: featureFlags.isWebEnabled && boolArg('web-enable-expression-evaluation'), webLaunchUrl: featureFlags.isWebEnabled ? stringArg('web-launch-url') : null, diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index 6f5f62263f7..8e190fa7824 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -986,6 +986,7 @@ class DebuggingOptions { this.webBrowserDebugPort, this.webBrowserFlags = const [], this.webChromeBinary, + this.webNoLaunchChrome = false, this.webEnableExpressionEvaluation = false, this.webLaunchUrl, bool? webCrossOriginIsolation, @@ -1022,6 +1023,7 @@ class DebuggingOptions { this.webBrowserDebugPort, this.webBrowserFlags = const [], this.webChromeBinary, + this.webNoLaunchChrome = false, this.webLaunchUrl, bool? webCrossOriginIsolation, WebRendererMode? webRenderer, @@ -1103,6 +1105,7 @@ class DebuggingOptions { required this.webBrowserDebugPort, required this.webBrowserFlags, required this.webChromeBinary, + required this.webNoLaunchChrome, required this.webEnableExpressionEvaluation, required this.webLaunchUrl, required this.webCrossOriginIsolation, @@ -1191,6 +1194,12 @@ class DebuggingOptions { /// Custom path to the Chrome executable. final String? webChromeBinary; + /// Whether to skip launching Chrome when running web apps. + /// + /// This is used by `flutter drive` to prevent launching a second Chrome + /// instance since the driver service launches Chrome separately. + final bool webNoLaunchChrome; + /// Enable expression evaluation for web target. final bool webEnableExpressionEvaluation; @@ -1301,6 +1310,7 @@ class DebuggingOptions { 'webBrowserDebugPort': webBrowserDebugPort, 'webBrowserFlags': webBrowserFlags, 'webChromeBinary': webChromeBinary, + 'webNoLaunchChrome': webNoLaunchChrome, 'webEnableExpressionEvaluation': webEnableExpressionEvaluation, 'webLaunchUrl': webLaunchUrl, 'webCrossOriginIsolation': webCrossOriginIsolation, @@ -1370,6 +1380,7 @@ class DebuggingOptions { webBrowserDebugPort: json['webBrowserDebugPort'] as int?, webBrowserFlags: (json['webBrowserFlags']! as List).cast(), webChromeBinary: json['webChromeBinary'] as String?, + webNoLaunchChrome: (json['webNoLaunchChrome'] as bool?) ?? false, webEnableExpressionEvaluation: json['webEnableExpressionEvaluation']! as bool, webLaunchUrl: json['webLaunchUrl'] as String?, webCrossOriginIsolation: json['webCrossOriginIsolation']! as bool, diff --git a/packages/flutter_tools/lib/src/drive/web_driver_service.dart b/packages/flutter_tools/lib/src/drive/web_driver_service.dart index 1f4091d9d7b..e5b1bc7bcaa 100644 --- a/packages/flutter_tools/lib/src/drive/web_driver_service.dart +++ b/packages/flutter_tools/lib/src/drive/web_driver_service.dart @@ -87,6 +87,7 @@ class WebDriverService extends DriverService { buildInfo, webDevServerConfig: debuggingOptions.webDevServerConfig, webChromeBinary: debuggingOptions.webChromeBinary, + webNoLaunchChrome: debuggingOptions.webNoLaunchChrome, webRenderer: debuggingOptions.webRenderer, webUseWasm: debuggingOptions.webUseWasm, ) @@ -94,6 +95,7 @@ class WebDriverService extends DriverService { buildInfo, webDevServerConfig: debuggingOptions.webDevServerConfig, webChromeBinary: debuggingOptions.webChromeBinary, + webNoLaunchChrome: debuggingOptions.webNoLaunchChrome, disablePortPublication: debuggingOptions.disablePortPublication, webRenderer: debuggingOptions.webRenderer, webUseWasm: debuggingOptions.webUseWasm, diff --git a/packages/flutter_tools/lib/src/web/web_device.dart b/packages/flutter_tools/lib/src/web/web_device.dart index f987f931b03..ef13cf85479 100644 --- a/packages/flutter_tools/lib/src/web/web_device.dart +++ b/packages/flutter_tools/lib/src/web/web_device.dart @@ -135,8 +135,7 @@ abstract class ChromiumDevice extends WebDevice { } else { url = platformArgs['uri']! as String; } - final launchChrome = platformArgs['no-launch-chrome'] != true; - if (launchChrome) { + if (!debuggingOptions.webNoLaunchChrome) { _chrome = await chromeLauncher.launch( url, cacheDir: _fileSystem.currentDirectory @@ -148,7 +147,7 @@ abstract class ChromiumDevice extends WebDevice { chromeBinary: debuggingOptions.webChromeBinary, ); } - _logger.sendEvent('app.webLaunchUrl', {'url': url, 'launched': launchChrome}); + _logger.sendEvent('app.webLaunchUrl', {'url': url, 'launched': !debuggingOptions.webNoLaunchChrome}); return LaunchResult.succeeded(vmServiceUri: Uri.parse(url)); } diff --git a/packages/flutter_tools/test/general.shard/web/devices_test.dart b/packages/flutter_tools/test/general.shard/web/devices_test.dart index a626461d7d7..6743b72e9a1 100644 --- a/packages/flutter_tools/test/general.shard/web/devices_test.dart +++ b/packages/flutter_tools/test/general.shard/web/devices_test.dart @@ -58,6 +58,52 @@ void main() { }, ); + testWithoutContext( + 'ChromiumDevice.startApp does not launch Chrome when webNoLaunchChrome is true', + () async { + final launcher = _TrackingChromiumLauncher(); + final chromiumDevice = _FakeChromiumDevice( + chromiumLauncher: launcher, + fileSystem: MemoryFileSystem.test(), + logger: BufferLogger.test(), + ); + + await chromiumDevice.startApp( + null, + debuggingOptions: DebuggingOptions.enabled( + BuildInfo.debug, + webNoLaunchChrome: true, + ), + platformArgs: {'uri': 'http://localhost:8080'}, + ); + + expect(launcher.launchCalled, isFalse); + }, + ); + + testWithoutContext( + 'ChromiumDevice.startApp launches Chrome when webNoLaunchChrome is false', + () async { + final launcher = _TrackingChromiumLauncher(); + final chromiumDevice = _FakeChromiumDevice( + chromiumLauncher: launcher, + fileSystem: MemoryFileSystem.test(), + logger: BufferLogger.test(), + ); + + await chromiumDevice.startApp( + null, + debuggingOptions: DebuggingOptions.enabled( + BuildInfo.debug, + webNoLaunchChrome: false, + ), + platformArgs: {'uri': 'http://localhost:8080'}, + ); + + expect(launcher.launchCalled, isTrue); + }, + ); + testWithoutContext('GoogleChromeDevice defaults', () async { final launcher = TestChromiumLauncher(); @@ -495,3 +541,44 @@ class _OnceClosableChromium extends Fake implements Chromium { } class _UnimplementedChromium extends Fake implements Chromium {} + +/// A test implementation of [ChromiumLauncher] that tracks whether launch() was called. +class _TrackingChromiumLauncher implements ChromiumLauncher { + bool launchCalled = false; + + @override + Completer currentCompleter = Completer(); + + @override + bool canFindExecutable() => true; + + @override + Future get connectedInstance => currentCompleter.future; + + @override + String findExecutable() => 'chrome'; + + @override + bool get hasChromeInstance => false; + + @override + Future launch( + String url, { + bool headless = false, + int? debugPort, + bool skipCheck = false, + Directory? cacheDir, + List webBrowserFlags = const [], + String? chromeBinary, + }) async { + launchCalled = true; + final chromium = _UnimplementedChromium(); + currentCompleter.complete(chromium); + return chromium; + } + + @override + Future connect(Chromium chrome, bool skipCheck) { + return currentCompleter.future; + } +}