diff --git a/cspell.config.yaml b/cspell.config.yaml index a1c4c7f0..676350fd 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -104,6 +104,7 @@ words: - vmcode - writeln - xcarchive + - xcarchives - xcframework - xcodebuild - xcodeproj diff --git a/packages/shorebird_cli/lib/src/artifact_manager.dart b/packages/shorebird_cli/lib/src/artifact_manager.dart index 85e6b98f..69e87cb4 100644 --- a/packages/shorebird_cli/lib/src/artifact_manager.dart +++ b/packages/shorebird_cli/lib/src/artifact_manager.dart @@ -282,6 +282,15 @@ class ArtifactManager { return archiveDirectory .listSync() .whereType() + // Get the most recently modified xcarchive to handle cases where an app + // may produce multiple xcarchives with different names. + // This still could grab the wrong ipa, if multiple `flutter` commands + // are running in parallel or the clock is/was broken on the machine. + // If either of those occurs in the wild we can check the contents of + // the xcarchives, but this should be good enough for now. + .sorted( + (a, b) => b.statSync().modified.compareTo(a.statSync().modified), + ) .firstWhereOrNull((directory) => directory.path.endsWith('.xcarchive')); } diff --git a/packages/shorebird_cli/test/src/artifact_manager_test.dart b/packages/shorebird_cli/test/src/artifact_manager_test.dart index 50941fdb..9e3e784d 100644 --- a/packages/shorebird_cli/test/src/artifact_manager_test.dart +++ b/packages/shorebird_cli/test/src/artifact_manager_test.dart @@ -593,6 +593,66 @@ void main() { }); }); + group( + 'when multiple xcarchive directories exist', + () { + late Directory oldArchiveDirectory; + late Directory newArchiveDirectory; + + setUp(() async { + oldArchiveDirectory = Directory( + p.join( + projectRoot.path, + 'build', + 'ios', + 'archive', + 'Runner.xcarchive', + ), + )..createSync(recursive: true); + // Wait to ensure the new archive directory is created after the old + // archive directory. + await Future.delayed(const Duration(milliseconds: 50)); + newArchiveDirectory = Directory( + p.join( + projectRoot.path, + 'build', + 'ios', + 'archive', + 'Runner2.xcarchive', + ), + )..createSync(recursive: true); + }); + + test('selects the most recently updated xcarchive', () async { + final firstResult = runWithOverrides( + () => artifactManager.getXcarchiveDirectory(), + ); + // The new archive directory should be selected because it was created + // after the old archive directory. + expect(firstResult!.path, equals(newArchiveDirectory.path)); + + // Now recreate the old archive directory and ensure it is selected. + oldArchiveDirectory.deleteSync(recursive: true); + oldArchiveDirectory = Directory( + p.join( + projectRoot.path, + 'build', + 'ios', + 'archive', + 'Runner.xcarchive', + ), + )..createSync(recursive: true); + final secondResult = runWithOverrides( + () => artifactManager.getXcarchiveDirectory(), + ); + expect(secondResult!.path, equals(oldArchiveDirectory.path)); + }); + }, + onPlatform: { + 'windows': const Skip('Flaky on Windows'), + }, + ); + group('when archive directory does not exist', () { test('returns null', () { expect(