From b3f36920ce9772af6e8bfb61563f607716de67ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Korde=C5=BE?= Date: Thu, 9 Jul 2026 13:40:18 +0200 Subject: [PATCH 1/4] Fix handling of executable paths and arguments containing spaces on Windows --- pkgs/hooks_runner/CHANGELOG.md | 2 + .../lib/src/utils/run_process.dart | 15 +-- .../test/utils/run_process_test.dart | 94 +++++++++++++++++++ 3 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 pkgs/hooks_runner/test/utils/run_process_test.dart diff --git a/pkgs/hooks_runner/CHANGELOG.md b/pkgs/hooks_runner/CHANGELOG.md index ca6126cb19..3bb99d87d9 100644 --- a/pkgs/hooks_runner/CHANGELOG.md +++ b/pkgs/hooks_runner/CHANGELOG.md @@ -1,6 +1,8 @@ ## 1.5.1-wip - Fix record_use path changing caching issue. +- Fix hook invocation when the Dart executable or an argument path contains a + space on Windows. ## 1.5.0 diff --git a/pkgs/hooks_runner/lib/src/utils/run_process.dart b/pkgs/hooks_runner/lib/src/utils/run_process.dart index 6f5707c793..269e7258cd 100644 --- a/pkgs/hooks_runner/lib/src/utils/run_process.dart +++ b/pkgs/hooks_runner/lib/src/utils/run_process.dart @@ -5,7 +5,7 @@ import 'dart:async'; import 'dart:developer'; import 'dart:io' - show Platform, Process, ProcessException, ProcessResult, systemEncoding; + show Process, ProcessException, ProcessResult, systemEncoding; import 'package:file/file.dart'; import 'package:logging/logging.dart'; @@ -32,11 +32,12 @@ Future runProcess({ final printWorkingDir = workingDirectory != null && workingDirectory != filesystem.currentDirectory.uri; + String quoteIfSpaced(String s) => s.contains(' ') ? '"$s"' : s; final commandString = [ if (printWorkingDir) '(cd ${workingDirectory.toFilePath()};', ...?environment?.entries.map((entry) => '${entry.key}=${entry.value}'), - executable.toFilePath(), - ...arguments.map((a) => a.contains(' ') ? "'$a'" : a), + quoteIfSpaced(executable.toFilePath()), + ...arguments.map(quoteIfSpaced), if (printWorkingDir) ')', ].join(' '); logger?.info('Running `$commandString`.'); @@ -58,9 +59,11 @@ Future runProcess({ workingDirectory: workingDirectory?.toFilePath(), environment: environment, includeParentEnvironment: includeParentEnvironment, - runInShell: - Platform.isWindows && - (!includeParentEnvironment || workingDirectory != null), + // Never run through a shell. On Windows, running an executable through + // `cmd.exe /c` mangles the command line when more than one argument is + // quoted (cmd strips the outer quotes when the line contains more than + // two quote characters), which breaks any invocation whose executable + // and arguments contain spaces. ); final stdoutSub = process.stdout.listen((List data) { diff --git a/pkgs/hooks_runner/test/utils/run_process_test.dart b/pkgs/hooks_runner/test/utils/run_process_test.dart new file mode 100644 index 0000000000..367aa51875 --- /dev/null +++ b/pkgs/hooks_runner/test/utils/run_process_test.dart @@ -0,0 +1,94 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Regression test for running commands whose executable path or arguments +// contain a space (e.g. the default pub cache under a Windows user name with +// a space, `C:\Users\First Last\AppData\Local\Pub\Cache\...`). +// +// On Windows, `runProcess` runs through `cmd.exe` (`runInShell`) whenever a +// `workingDirectory` is passed. `cmd.exe`'s `/c` quote-stripping rule mangles +// the command line as soon as more than one token needs quoting because it +// contains a space (the executable path and an argument, or two arguments), +// causing errors like +// `'C:\Program' is not recognized as an internal or external command`. + +import 'dart:io'; + +import 'package:test/test.dart'; + +import '../helpers.dart'; + +void main() { + late Uri scriptUri; + + setUpAll(() async { + final scriptDir = await tempDirForTest(); + scriptUri = scriptDir.resolve('echo_args.dart'); + await File.fromUri(scriptUri).writeAsString(''' +void main(List args) { + print('ARGC:\${args.length}'); + print('ARGV:\${args.join('|')}'); +} +'''); + }); + + test( + 'runProcess handles an executable path containing a space', + timeout: const Timeout.factor(5), + () async { + final outDir = await tempDirForTest(prefix: 'run process space '); + final exeUri = outDir.resolve( + 'echo args${Platform.isWindows ? '.exe' : ''}', + ); + final compileResult = await Process.run(Platform.resolvedExecutable, [ + 'compile', + 'exe', + scriptUri.toFilePath(), + '-o', + exeUri.toFilePath(), + ]); + expect(compileResult.exitCode, 0, reason: '${compileResult.stderr}'); + + final workingDir = await tempDirForTest(); + final result = await runProcess( + executable: exeUri, + arguments: ['an argument with spaces'], + workingDirectory: workingDir, + logger: logger, + ); + + expect(result.exitCode, 0); + expect(result.stdout, contains('ARGC:1')); + expect(result.stdout, contains('ARGV:an argument with spaces')); + }, + ); + + test('runProcess handles arguments containing a space', () async { + final workingDir = await tempDirForTest(); + final result = await runProcess( + executable: Uri.file(Platform.resolvedExecutable), + arguments: [scriptUri.toFilePath(), 'first arg', 'second arg'], + workingDirectory: workingDir, + logger: logger, + ); + + expect(result.exitCode, 0); + expect(result.stdout, contains('ARGC:2')); + expect(result.stdout, contains('ARGV:first arg|second arg')); + }); + + test('runProcess handles arguments containing a space and quotes', () async { + final workingDir = await tempDirForTest(); + final result = await runProcess( + executable: Uri.file(Platform.resolvedExecutable), + arguments: [scriptUri.toFilePath(), 'fir"st arg', 'sec\'ond arg'], + workingDirectory: workingDir, + logger: logger, + ); + + expect(result.exitCode, 0); + expect(result.stdout, contains('ARGC:2')); + expect(result.stdout, contains('ARGV:fir"st arg|sec\'ond arg')); + }); +} From 11f9f053981a4b55cbb208c81c62c91cab26a42b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Korde=C5=BE?= Date: Thu, 9 Jul 2026 16:35:06 +0200 Subject: [PATCH 2/4] Reformat run_process.dart --- pkgs/hooks_runner/lib/src/utils/run_process.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkgs/hooks_runner/lib/src/utils/run_process.dart b/pkgs/hooks_runner/lib/src/utils/run_process.dart index 269e7258cd..c949fad58a 100644 --- a/pkgs/hooks_runner/lib/src/utils/run_process.dart +++ b/pkgs/hooks_runner/lib/src/utils/run_process.dart @@ -4,8 +4,7 @@ import 'dart:async'; import 'dart:developer'; -import 'dart:io' - show Process, ProcessException, ProcessResult, systemEncoding; +import 'dart:io' show Process, ProcessException, ProcessResult, systemEncoding; import 'package:file/file.dart'; import 'package:logging/logging.dart'; From 971e68f14fdb7bdbc853dfe12f5c791d7ee2a6b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Korde=C5=BE?= Date: Fri, 10 Jul 2026 10:08:51 +0200 Subject: [PATCH 3/4] Update temp directory naming to handle spaces --- pkgs/hooks_runner/test/helpers.dart | 12 ++++++++++-- pkgs/hooks_runner/test/utils/run_process_test.dart | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/pkgs/hooks_runner/test/helpers.dart b/pkgs/hooks_runner/test/helpers.dart index bb783a5295..76cc881ddf 100644 --- a/pkgs/hooks_runner/test/helpers.dart +++ b/pkgs/hooks_runner/test/helpers.dart @@ -33,7 +33,11 @@ Future inTempDir( String? prefix, bool keepTemp = false, }) async { - final tempDir = await Directory.systemTemp.createTemp(prefix); + final basePrefix = prefix ?? 'hooks_runner_test'; + final effectivePrefix = basePrefix.contains(' ') + ? basePrefix + : '$basePrefix with spaces '; + final tempDir = await Directory.systemTemp.createTemp(effectivePrefix); // Deal with Windows temp folder aliases. final tempUri = Directory( await tempDir.resolveSymbolicLinks(), @@ -58,7 +62,11 @@ Future inTempDir( } Future tempDirForTest({String? prefix, bool keepTemp = false}) async { - final tempDir = await Directory.systemTemp.createTemp(prefix); + final basePrefix = prefix ?? 'hooks_runner_test'; + final effectivePrefix = basePrefix.contains(' ') + ? basePrefix + : '$basePrefix with spaces '; + final tempDir = await Directory.systemTemp.createTemp(effectivePrefix); // Deal with Windows temp folder aliases. final tempUri = Directory( await tempDir.resolveSymbolicLinks(), diff --git a/pkgs/hooks_runner/test/utils/run_process_test.dart b/pkgs/hooks_runner/test/utils/run_process_test.dart index 367aa51875..25686c6786 100644 --- a/pkgs/hooks_runner/test/utils/run_process_test.dart +++ b/pkgs/hooks_runner/test/utils/run_process_test.dart @@ -37,7 +37,7 @@ void main(List args) { 'runProcess handles an executable path containing a space', timeout: const Timeout.factor(5), () async { - final outDir = await tempDirForTest(prefix: 'run process space '); + final outDir = await tempDirForTest(); final exeUri = outDir.resolve( 'echo args${Platform.isWindows ? '.exe' : ''}', ); From fd3997ad2e24798cee390b38166118890c456292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Korde=C5=BE?= Date: Fri, 10 Jul 2026 12:32:46 +0200 Subject: [PATCH 4/4] Add support for disabling spaces in paths in temp directory functions --- .../test/build_runner/version_skew_test.dart | 4 ++-- pkgs/hooks_runner/test/helpers.dart | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/pkgs/hooks_runner/test/build_runner/version_skew_test.dart b/pkgs/hooks_runner/test/build_runner/version_skew_test.dart index 683407b839..bc6e313cb7 100644 --- a/pkgs/hooks_runner/test/build_runner/version_skew_test.dart +++ b/pkgs/hooks_runner/test/build_runner/version_skew_test.dart @@ -30,7 +30,7 @@ void main() async { )).success; expect(result.encodedAssets.length, 1); } - }); + }, useSpacesInPath: false); }, ); @@ -55,7 +55,7 @@ void main() async { logMessages.join('\n'), stringContainsInOrder(['Unhandled exception']), ); - }); + }, useSpacesInPath: false); }, ); } diff --git a/pkgs/hooks_runner/test/helpers.dart b/pkgs/hooks_runner/test/helpers.dart index 76cc881ddf..1807d81c14 100644 --- a/pkgs/hooks_runner/test/helpers.dart +++ b/pkgs/hooks_runner/test/helpers.dart @@ -32,9 +32,10 @@ Future inTempDir( Future Function(Uri tempUri) fun, { String? prefix, bool keepTemp = false, + bool useSpacesInPath = true, }) async { final basePrefix = prefix ?? 'hooks_runner_test'; - final effectivePrefix = basePrefix.contains(' ') + final effectivePrefix = (!useSpacesInPath || basePrefix.contains(' ')) ? basePrefix : '$basePrefix with spaces '; final tempDir = await Directory.systemTemp.createTemp(effectivePrefix); @@ -61,9 +62,13 @@ Future inTempDir( } } -Future tempDirForTest({String? prefix, bool keepTemp = false}) async { +Future tempDirForTest({ + String? prefix, + bool keepTemp = false, + bool useSpacesInPath = true, +}) async { final basePrefix = prefix ?? 'hooks_runner_test'; - final effectivePrefix = basePrefix.contains(' ') + final effectivePrefix = (!useSpacesInPath || basePrefix.contains(' ')) ? basePrefix : '$basePrefix with spaces '; final tempDir = await Directory.systemTemp.createTemp(effectivePrefix);