From 15daeaf30ad495fa338f4c3b4e3c5b31b7653094 Mon Sep 17 00:00:00 2001 From: Eric Seidel Date: Sat, 21 Mar 2026 08:51:25 -0700 Subject: [PATCH] fix: select correct executable in Windows preview (#3492) --- .../lib/src/commands/preview_command.dart | 4 ++-- .../lib/src/platform/windows/windows.dart | 19 ++++++++++++++++--- .../src/commands/preview_command_test.dart | 14 ++++++++++++++ 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/shorebird_cli/lib/src/commands/preview_command.dart b/packages/shorebird_cli/lib/src/commands/preview_command.dart index 611342da..42c9dc91 100644 --- a/packages/shorebird_cli/lib/src/commands/preview_command.dart +++ b/packages/shorebird_cli/lib/src/commands/preview_command.dart @@ -462,8 +462,8 @@ This is only applicable when previewing Android releases.''', channel: track.channel, ); - final exeFile = appDirectory.listSync().whereType().firstWhere( - (file) => file.path.endsWith('.exe'), + final exeFile = windows.findExecutable( + releaseDirectory: appDirectory, ); return startAndForwardOutput(exeFile.path); diff --git a/packages/shorebird_cli/lib/src/platform/windows/windows.dart b/packages/shorebird_cli/lib/src/platform/windows/windows.dart index 7ca787bd..e8f24ff3 100644 --- a/packages/shorebird_cli/lib/src/platform/windows/windows.dart +++ b/packages/shorebird_cli/lib/src/platform/windows/windows.dart @@ -1,3 +1,4 @@ +// cspell:words crashpad import 'dart:io'; import 'package:collection/collection.dart'; @@ -19,19 +20,31 @@ final windowsRef = create(Windows.new); /// The [Windows] instance available in the current zone. Windows get windows => read(windowsRef); +/// Executables bundled with Flutter Windows builds that are not the app. +const defaultIgnoredExecutables = {'crashpad_handler.exe'}; + /// A class that provides Windows-specific functionality. class Windows { /// Returns the selected application `.exe` from [releaseDirectory]. - /// Searches for an exact match for [projectName] and if none is found, - /// falls back to returning the most recently modified executable. + /// If [projectName] is provided, searches for an exact match first. + /// Falls back to returning the most recently modified executable, + /// excluding [ignoredExecutables] (defaults to [defaultIgnoredExecutables]). + /// + /// Note: when files are extracted from a zip, they may all have similar + /// timestamps, making the mtime fallback unreliable. Prefer passing + /// [projectName] when available. File findExecutable({ required Directory releaseDirectory, - required String projectName, + String? projectName, + Set ignoredExecutables = defaultIgnoredExecutables, }) { final executables = releaseDirectory .listSync() .whereType() .where((f) => p.extension(f.path).toLowerCase() == '.exe') + .where( + (f) => !ignoredExecutables.contains(p.basename(f.path)), + ) .sorted((a, b) => b.lastModifiedSync().compareTo(a.lastModifiedSync())); if (executables.isEmpty) { diff --git a/packages/shorebird_cli/test/src/commands/preview_command_test.dart b/packages/shorebird_cli/test/src/commands/preview_command_test.dart index 77cfcfa4..768fc3e2 100644 --- a/packages/shorebird_cli/test/src/commands/preview_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/preview_command_test.dart @@ -2388,6 +2388,7 @@ channel: ${DeploymentTrack.staging.channel} late ShorebirdProcess shorebirdProcess; late Process process; late Directory windowsReleaseDirectory; + late Windows windowsMock; R runWithOverrides(R Function() body) { return HttpOverrides.runZoned( @@ -2406,6 +2407,7 @@ channel: ${DeploymentTrack.staging.channel} processRef.overrideWith(() => shorebirdProcess), shorebirdEnvRef.overrideWith(() => shorebirdEnv), shorebirdValidatorRef.overrideWith(() => shorebirdValidator), + windowsRef.overrideWith(() => windowsMock), }, ), ); @@ -2435,6 +2437,18 @@ channel: ${DeploymentTrack.staging.channel} setUp(() { shorebirdProcess = MockShorebirdProcess(); process = MockProcess(); + windowsMock = MockWindows(); + when( + () => windowsMock.findExecutable( + releaseDirectory: any(named: 'releaseDirectory'), + projectName: any(named: 'projectName'), + ), + ).thenAnswer((invocation) { + final dir = invocation.namedArguments[#releaseDirectory] as Directory; + return dir.listSync().whereType().firstWhere( + (f) => f.path.endsWith('.exe'), + ); + }); windowsReleaseArtifact = MockReleaseArtifact(); final tempDir = Directory.systemTemp.createTempSync();