fix(windows): select correct executable when multiple executables are generated (#3319)
Co-authored-by: Felix Angelov <felix@shorebird.dev> Co-authored-by: Bryan Oltman <bryan@shorebird.dev>
This commit is contained in:
@@ -88,6 +88,7 @@ Command: shorebird ${args.join(' ')}
|
||||
shorebirdToolsRef,
|
||||
shorebirdValidatorRef,
|
||||
shorebirdVersionRef,
|
||||
windowsRef,
|
||||
xcodeBuildRef,
|
||||
},
|
||||
),
|
||||
|
||||
@@ -19,6 +19,7 @@ import 'package:shorebird_cli/src/logging/logging.dart';
|
||||
import 'package:shorebird_cli/src/patch_diff_checker.dart';
|
||||
import 'package:shorebird_cli/src/platform/platform.dart';
|
||||
import 'package:shorebird_cli/src/release_type.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_env.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_validator.dart';
|
||||
import 'package:shorebird_cli/src/third_party/flutter_tools/lib/flutter_tools.dart';
|
||||
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
|
||||
@@ -144,9 +145,10 @@ class WindowsPatcher extends Patcher {
|
||||
zipFile: artifact,
|
||||
outputDirectory: outputDirectory,
|
||||
);
|
||||
final exeFile = outputDirectory.listSync().whereType<File>().firstWhere(
|
||||
(file) => p.extension(file.path) == '.exe',
|
||||
final executable = windows.findExecutable(
|
||||
releaseDirectory: outputDirectory,
|
||||
projectName: shorebirdEnv.getPubspecYaml()!.name,
|
||||
);
|
||||
return powershell.getExeVersionString(exeFile);
|
||||
return powershell.getProductVersion(executable);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:platform/platform.dart';
|
||||
import 'package:shorebird_cli/src/archive/archive.dart';
|
||||
import 'package:shorebird_cli/src/artifact_builder/artifact_builder.dart';
|
||||
@@ -79,14 +78,11 @@ To change the version of this release, change your app's version in your pubspec
|
||||
Future<String> getReleaseVersion({
|
||||
required FileSystemEntity releaseArtifactRoot,
|
||||
}) {
|
||||
final exe = (releaseArtifactRoot as Directory)
|
||||
.listSync()
|
||||
.whereType<File>()
|
||||
.firstWhere(
|
||||
(entity) => p.extension(entity.path) == '.exe',
|
||||
orElse: () => throw Exception('No .exe found in release artifact'),
|
||||
);
|
||||
return powershell.getExeVersionString(exe);
|
||||
final executable = windows.findExecutable(
|
||||
releaseDirectory: releaseArtifactRoot as Directory,
|
||||
projectName: shorebirdEnv.getPubspecYaml()!.name,
|
||||
);
|
||||
return powershell.getProductVersion(executable);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:scoped_deps/scoped_deps.dart';
|
||||
import 'package:shorebird_cli/src/logging/logging.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_process.dart';
|
||||
|
||||
/// A reference to a [Powershell] instance.
|
||||
@@ -20,6 +21,7 @@ class Powershell {
|
||||
List<String> arguments, {
|
||||
String? workingDirectory,
|
||||
}) async {
|
||||
logger.detail('[powershell] Command: $executable ${arguments.join(' ')}');
|
||||
final result = await process.run(executable, arguments);
|
||||
if (result.exitCode != ExitCode.success.code) {
|
||||
throw ProcessException(
|
||||
@@ -33,12 +35,11 @@ class Powershell {
|
||||
}
|
||||
|
||||
/// Returns the version string of the given executable file.
|
||||
Future<String> getExeVersionString(File exeFile) async {
|
||||
final exePath = exeFile.path;
|
||||
final pwshCommand =
|
||||
"(Get-Item -Path '$exePath').VersionInfo.ProductVersion";
|
||||
|
||||
final result = await pwsh(['-Command', pwshCommand]);
|
||||
Future<String> getProductVersion(File executable) async {
|
||||
final result = await pwsh([
|
||||
'-Command',
|
||||
"(Get-Item -Path '${executable.path}').VersionInfo.ProductVersion",
|
||||
]);
|
||||
|
||||
return (result.stdout as String).trim();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:pub_semver/pub_semver.dart';
|
||||
import 'package:scoped_deps/scoped_deps.dart';
|
||||
|
||||
/// The primary release artifact architecture for Windows releases.
|
||||
/// This is a zipped copy of `build/windows/x64/runner/Release`, which is
|
||||
@@ -11,3 +16,35 @@ const primaryWindowsReleaseArtifactArch = 'win_archive';
|
||||
|
||||
/// The minimum allowed Flutter version for creating Windows releases.
|
||||
final minimumSupportedWindowsFlutterVersion = Version(3, 32, 6);
|
||||
|
||||
/// A reference to a [Windows] instance.
|
||||
final windowsRef = create(Windows.new);
|
||||
|
||||
/// The [Windows] instance available in the current zone.
|
||||
Windows get windows => read(windowsRef);
|
||||
|
||||
/// 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.
|
||||
File findExecutable({
|
||||
required Directory releaseDirectory,
|
||||
required String projectName,
|
||||
}) {
|
||||
final executables = releaseDirectory
|
||||
.listSync()
|
||||
.whereType<File>()
|
||||
.where((f) => p.extension(f.path).toLowerCase() == '.exe')
|
||||
.sorted((a, b) => b.lastModifiedSync().compareTo(a.lastModifiedSync()));
|
||||
|
||||
if (executables.isEmpty) {
|
||||
throw Exception('No executables found in ${releaseDirectory.path}');
|
||||
}
|
||||
|
||||
return executables.firstWhere(
|
||||
(e) => p.basenameWithoutExtension(e.path) == projectName,
|
||||
orElse: () => executables.first,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:platform/platform.dart';
|
||||
import 'package:pubspec_parse/pubspec_parse.dart';
|
||||
import 'package:scoped_deps/scoped_deps.dart';
|
||||
import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart';
|
||||
import 'package:shorebird_cli/src/artifact_builder/artifact_builder.dart';
|
||||
@@ -55,6 +56,7 @@ void main() {
|
||||
late ShorebirdEnv shorebirdEnv;
|
||||
late ShorebirdFlutter shorebirdFlutter;
|
||||
late ShorebirdValidator shorebirdValidator;
|
||||
late Windows windows;
|
||||
late WindowsPatcher patcher;
|
||||
|
||||
R runWithOverrides<R>(R Function() body) {
|
||||
@@ -75,6 +77,7 @@ void main() {
|
||||
shorebirdEnvRef.overrideWith(() => shorebirdEnv),
|
||||
shorebirdFlutterRef.overrideWith(() => shorebirdFlutter),
|
||||
shorebirdValidatorRef.overrideWith(() => shorebirdValidator),
|
||||
windowsRef.overrideWith(() => windows),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -108,6 +111,7 @@ void main() {
|
||||
shorebirdEnv = MockShorebirdEnv();
|
||||
shorebirdFlutter = MockShorebirdFlutter();
|
||||
shorebirdValidator = MockShorebirdValidator();
|
||||
windows = MockWindows();
|
||||
|
||||
when(() => argParser.options).thenReturn({});
|
||||
|
||||
@@ -511,7 +515,18 @@ void main() {
|
||||
});
|
||||
|
||||
group('extractReleaseVersionFromArtifact', () {
|
||||
const projectName = 'my_app';
|
||||
const productVersion = '1.2.3';
|
||||
|
||||
late File executable;
|
||||
late Pubspec pubspec;
|
||||
|
||||
setUp(() async {
|
||||
executable = File(p.join(projectRoot.path, 'my_app.exe'));
|
||||
pubspec = MockPubspec();
|
||||
|
||||
when(() => shorebirdEnv.getPubspecYaml()).thenReturn(pubspec);
|
||||
when(() => pubspec.name).thenReturn(projectName);
|
||||
when(
|
||||
() => artifactManager.extractZip(
|
||||
zipFile: any(named: 'zipFile'),
|
||||
@@ -525,16 +540,29 @@ void main() {
|
||||
).createSync(recursive: true);
|
||||
});
|
||||
when(
|
||||
() => powershell.getExeVersionString(any()),
|
||||
).thenAnswer((_) async => '1.2.3');
|
||||
() => windows.findExecutable(
|
||||
releaseDirectory: any(named: 'releaseDirectory'),
|
||||
projectName: any(named: 'projectName'),
|
||||
),
|
||||
).thenReturn(executable);
|
||||
when(
|
||||
() => powershell.getProductVersion(any()),
|
||||
).thenAnswer((_) async => productVersion);
|
||||
});
|
||||
|
||||
test('returns version from archived exe', () async {
|
||||
test('returns correct version from archived executable', () async {
|
||||
final version = await runWithOverrides(
|
||||
() => patcher.extractReleaseVersionFromArtifact(File('')),
|
||||
() => patcher.extractReleaseVersionFromArtifact(executable),
|
||||
);
|
||||
|
||||
expect(version, '1.2.3');
|
||||
expect(version, equals(productVersion));
|
||||
verify(
|
||||
() => windows.findExecutable(
|
||||
releaseDirectory: any(named: 'releaseDirectory'),
|
||||
projectName: projectName,
|
||||
),
|
||||
).called(1);
|
||||
verify(() => powershell.getProductVersion(executable)).called(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:platform/platform.dart';
|
||||
import 'package:pubspec_parse/pubspec_parse.dart';
|
||||
import 'package:scoped_deps/scoped_deps.dart';
|
||||
import 'package:shorebird_cli/src/artifact_builder/artifact_builder.dart';
|
||||
import 'package:shorebird_cli/src/artifact_manager.dart';
|
||||
@@ -15,6 +16,7 @@ import 'package:shorebird_cli/src/common_arguments.dart';
|
||||
import 'package:shorebird_cli/src/doctor.dart';
|
||||
import 'package:shorebird_cli/src/executables/executables.dart';
|
||||
import 'package:shorebird_cli/src/logging/logging.dart';
|
||||
import 'package:shorebird_cli/src/platform/platform.dart';
|
||||
import 'package:shorebird_cli/src/release_type.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_env.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_flutter.dart';
|
||||
@@ -45,6 +47,7 @@ void main() {
|
||||
late ShorebirdEnv shorebirdEnv;
|
||||
late ShorebirdFlutter shorebirdFlutter;
|
||||
late ShorebirdValidator shorebirdValidator;
|
||||
late Windows windows;
|
||||
late WindowsReleaser releaser;
|
||||
|
||||
R runWithOverrides<R>(R Function() body) {
|
||||
@@ -62,6 +65,7 @@ void main() {
|
||||
shorebirdEnvRef.overrideWith(() => shorebirdEnv),
|
||||
shorebirdFlutterRef.overrideWith(() => shorebirdFlutter),
|
||||
shorebirdValidatorRef.overrideWith(() => shorebirdValidator),
|
||||
windowsRef.overrideWith(() => windows),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -88,6 +92,7 @@ void main() {
|
||||
shorebirdEnv = MockShorebirdEnv();
|
||||
shorebirdFlutter = MockShorebirdFlutter();
|
||||
shorebirdValidator = MockShorebirdValidator();
|
||||
windows = MockWindows();
|
||||
|
||||
when(() => argResults.rest).thenReturn([]);
|
||||
when(() => argResults.wasParsed(any())).thenReturn(false);
|
||||
@@ -324,7 +329,24 @@ To change the version of this release, change your app's version in your pubspec
|
||||
});
|
||||
|
||||
group('getReleaseVersion', () {
|
||||
group('when exe does not exist', () {
|
||||
const projectName = 'my_app';
|
||||
|
||||
late Pubspec pubspec;
|
||||
|
||||
setUp(() {
|
||||
pubspec = MockPubspec();
|
||||
|
||||
when(
|
||||
() => windows.findExecutable(
|
||||
releaseDirectory: any(named: 'releaseDirectory'),
|
||||
projectName: any(named: 'projectName'),
|
||||
),
|
||||
).thenThrow(Exception('No .exe found in release artifact'));
|
||||
when(() => shorebirdEnv.getPubspecYaml()).thenReturn(pubspec);
|
||||
when(() => pubspec.name).thenReturn(projectName);
|
||||
});
|
||||
|
||||
group('when an executable does not exist', () {
|
||||
test('throws exception', () {
|
||||
expect(
|
||||
() => runWithOverrides(
|
||||
@@ -336,22 +358,39 @@ To change the version of this release, change your app's version in your pubspec
|
||||
});
|
||||
});
|
||||
|
||||
group('when exe exists', () {
|
||||
group('when an executable exists', () {
|
||||
const productVersion = '1.2.3';
|
||||
late File executable;
|
||||
|
||||
setUp(() {
|
||||
File(p.join(projectRoot.path, 'app.exe')).createSync();
|
||||
executable = File(p.join(projectRoot.path, 'app.exe'));
|
||||
when(
|
||||
() => powershell.getExeVersionString(any()),
|
||||
).thenAnswer((_) async => '1.2.3');
|
||||
() => windows.findExecutable(
|
||||
releaseDirectory: any(named: 'releaseDirectory'),
|
||||
projectName: any(named: 'projectName'),
|
||||
),
|
||||
).thenReturn(executable);
|
||||
when(
|
||||
() => powershell.getProductVersion(any()),
|
||||
).thenAnswer((_) async => productVersion);
|
||||
});
|
||||
|
||||
test('returns result of getExeVersionString', () async {
|
||||
test('returns result of powershell.getProductVersion', () async {
|
||||
await expectLater(
|
||||
runWithOverrides(
|
||||
() =>
|
||||
releaser.getReleaseVersion(releaseArtifactRoot: projectRoot),
|
||||
() => releaser.getReleaseVersion(
|
||||
releaseArtifactRoot: projectRoot,
|
||||
),
|
||||
),
|
||||
completion(equals('1.2.3')),
|
||||
completion(equals(productVersion)),
|
||||
);
|
||||
verify(
|
||||
() => windows.findExecutable(
|
||||
releaseDirectory: projectRoot,
|
||||
projectName: projectName,
|
||||
),
|
||||
).called(1);
|
||||
verify(() => powershell.getProductVersion(executable)).called(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:io';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:scoped_deps/scoped_deps.dart';
|
||||
import 'package:shorebird_cli/src/executables/executables.dart';
|
||||
import 'package:shorebird_cli/src/logging/logging.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_process.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
@@ -10,6 +11,7 @@ import '../mocks.dart';
|
||||
|
||||
void main() {
|
||||
group(Powershell, () {
|
||||
late ShorebirdLogger logger;
|
||||
late ShorebirdProcessResult processResult;
|
||||
late ShorebirdProcess process;
|
||||
late Powershell powershell;
|
||||
@@ -17,11 +19,15 @@ void main() {
|
||||
R runWithOverrides<R>(R Function() body) {
|
||||
return runScoped(
|
||||
() => body(),
|
||||
values: {processRef.overrideWith(() => process)},
|
||||
values: {
|
||||
loggerRef.overrideWith(() => logger),
|
||||
processRef.overrideWith(() => process),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
setUp(() {
|
||||
logger = MockShorebirdLogger();
|
||||
processResult = MockShorebirdProcessResult();
|
||||
process = MockShorebirdProcess();
|
||||
|
||||
@@ -36,7 +42,7 @@ void main() {
|
||||
powershell = runWithOverrides(Powershell.new);
|
||||
});
|
||||
|
||||
group('getExeVersionString', () {
|
||||
group('getProductVersion', () {
|
||||
group('when exit code is not success', () {
|
||||
setUp(() {
|
||||
when(() => processResult.exitCode).thenReturn(1);
|
||||
@@ -44,7 +50,7 @@ void main() {
|
||||
|
||||
test('throws an exception', () async {
|
||||
await expectLater(
|
||||
runWithOverrides(() => powershell.getExeVersionString(File(''))),
|
||||
runWithOverrides(() => powershell.getProductVersion(File(''))),
|
||||
throwsA(isA<Exception>()),
|
||||
);
|
||||
});
|
||||
@@ -59,7 +65,7 @@ void main() {
|
||||
|
||||
test('returns unaltered version string', () async {
|
||||
final version = await runWithOverrides(
|
||||
() => powershell.getExeVersionString(File('')),
|
||||
() => powershell.getProductVersion(File('')),
|
||||
);
|
||||
expect(version, '1.0.0+1');
|
||||
});
|
||||
@@ -76,7 +82,7 @@ void main() {
|
||||
'directory with spaces',
|
||||
);
|
||||
final file = File('${directory.path}/file.exe');
|
||||
await runWithOverrides(() => powershell.getExeVersionString(file));
|
||||
await runWithOverrides(() => powershell.getProductVersion(file));
|
||||
verify(
|
||||
() => process.run('powershell.exe', [
|
||||
'-Command',
|
||||
@@ -96,7 +102,7 @@ void main() {
|
||||
'returns the version string without a build number',
|
||||
() async {
|
||||
final version = await runWithOverrides(
|
||||
() => powershell.getExeVersionString(File('')),
|
||||
() => powershell.getProductVersion(File('')),
|
||||
);
|
||||
expect(version, '1.0.0');
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:jwt/jwt.dart';
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:platform/platform.dart';
|
||||
import 'package:pubspec_parse/pubspec_parse.dart';
|
||||
import 'package:shorebird_cli/src/android_sdk.dart';
|
||||
import 'package:shorebird_cli/src/android_studio.dart';
|
||||
import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart';
|
||||
@@ -148,6 +149,8 @@ class MockProcess extends Mock implements Process {}
|
||||
|
||||
class MockProgress extends Mock implements Progress {}
|
||||
|
||||
class MockPubspec extends Mock implements Pubspec {}
|
||||
|
||||
class MockPubspecEditor extends Mock implements PubspecEditor {}
|
||||
|
||||
class MockRelease extends Mock implements Release {}
|
||||
@@ -187,4 +190,6 @@ class MockStdout extends Mock implements Stdout {}
|
||||
|
||||
class MockValidator extends Mock implements Validator {}
|
||||
|
||||
class MockWindows extends Mock implements Windows {}
|
||||
|
||||
class MockXcodeBuild extends Mock implements XcodeBuild {}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:scoped_deps/scoped_deps.dart';
|
||||
import 'package:shorebird_cli/src/platform/windows.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
R runWithOverrides<R>(R Function() body) {
|
||||
return runScoped(
|
||||
body,
|
||||
values: {windowsRef.overrideWith(Windows.new)},
|
||||
);
|
||||
}
|
||||
|
||||
group(Windows, () {
|
||||
group('findExecutable', () {
|
||||
const projectName = 'my_app';
|
||||
late Directory tempDir;
|
||||
|
||||
setUp(() {
|
||||
tempDir = Directory.systemTemp.createTempSync();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
tempDir.deleteSync(recursive: true);
|
||||
});
|
||||
|
||||
group('when no executables exist', () {
|
||||
test('throws an exception', () {
|
||||
expect(
|
||||
() => runWithOverrides(
|
||||
() => windows.findExecutable(
|
||||
releaseDirectory: tempDir,
|
||||
projectName: projectName,
|
||||
),
|
||||
),
|
||||
throwsA(isA<Exception>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when an exact match exists', () {
|
||||
late File app;
|
||||
|
||||
setUp(() {
|
||||
app = File(p.join(tempDir.path, '$projectName.exe'))..createSync();
|
||||
File(p.join(tempDir.path, 'other.exe')).createSync();
|
||||
});
|
||||
|
||||
test('returns it', () {
|
||||
final executable = runWithOverrides(
|
||||
() => windows.findExecutable(
|
||||
releaseDirectory: tempDir,
|
||||
projectName: projectName,
|
||||
),
|
||||
);
|
||||
expect(executable.path, equals(app.path));
|
||||
});
|
||||
});
|
||||
|
||||
group('when an exact match does not exist', () {
|
||||
late File app;
|
||||
|
||||
setUp(() async {
|
||||
File(p.join(tempDir.path, 'other.exe')).createSync();
|
||||
// Ensure my_app is created after other.
|
||||
await Future<void>.delayed(const Duration(seconds: 1));
|
||||
app = File(p.join(tempDir.path, '$projectName.exe'))..createSync();
|
||||
});
|
||||
|
||||
test('returns most recently modified executable', () {
|
||||
final selected = runWithOverrides(
|
||||
() => windows.findExecutable(
|
||||
releaseDirectory: tempDir,
|
||||
projectName: 'runner',
|
||||
),
|
||||
);
|
||||
expect(selected.path, equals(app.path));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user