From 35746dad7e2592cb9849e6e6f23c79e2b50375c5 Mon Sep 17 00:00:00 2001 From: Brandon DeRosier Date: Mon, 4 May 2026 16:24:59 -0700 Subject: [PATCH] refactor(cli): consolidate Apple releaser/patcher duplication into mixins (#3735) --- .../commands/patch/apple_patcher_mixin.dart | 133 ++++++++++++++++++ .../commands/patch/ios_framework_patcher.dart | 46 ++---- .../lib/src/commands/patch/ios_patcher.dart | 117 +++------------ .../lib/src/commands/patch/macos_patcher.dart | 89 ++---------- .../release/apple_releaser_mixin.dart | 57 ++++++++ .../release/ios_framework_releaser.dart | 34 +---- .../src/commands/release/ios_releaser.dart | 55 ++------ .../src/commands/release/macos_releaser.dart | 57 ++------ .../commands/release/ios_releaser_test.dart | 14 +- .../commands/release/macos_releaser_test.dart | 15 +- 10 files changed, 256 insertions(+), 361 deletions(-) create mode 100644 packages/shorebird_cli/lib/src/commands/patch/apple_patcher_mixin.dart create mode 100644 packages/shorebird_cli/lib/src/commands/release/apple_releaser_mixin.dart diff --git a/packages/shorebird_cli/lib/src/commands/patch/apple_patcher_mixin.dart b/packages/shorebird_cli/lib/src/commands/patch/apple_patcher_mixin.dart new file mode 100644 index 00000000..a49cb52a --- /dev/null +++ b/packages/shorebird_cli/lib/src/commands/patch/apple_patcher_mixin.dart @@ -0,0 +1,133 @@ +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'package:shorebird_cli/src/archive_analysis/apple_archive_differ.dart'; +import 'package:shorebird_cli/src/commands/patch/patch.dart'; +import 'package:shorebird_cli/src/executables/xcodebuild.dart'; +import 'package:shorebird_cli/src/logging/logging.dart'; +import 'package:shorebird_cli/src/metadata/metadata.dart'; +import 'package:shorebird_cli/src/patch_diff_checker.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_cli/src/validators/validators.dart'; +import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; +import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart'; + +/// Shared logic for Apple-platform patchers (iOS, macOS, iOS framework). +/// +/// Concrete patchers supply the platform-specific validators and (for iOS and +/// macOS) the local Podfile.lock state used to detect native diffs that +/// `package:archive` can't reliably catch in a nondeterministic Xcode build. +mixin ApplePatcherMixin on Patcher { + /// The name of the file that gen_snapshot writes split debug info to. The + /// filename is iOS-flavored historically; we keep it for both iOS and macOS + /// so existing release artifacts remain referenceable. + static const splitDebugInfoFileName = 'app.ios-arm64.symbols'; + + /// Resolves the absolute path inside [directory] where gen_snapshot should + /// write the split debug info file. + static String saveDebuggingInfoPath(String directory) => + p.join(p.absolute(directory), splitDebugInfoFileName); + + /// The additional gen_snapshot arguments to pass when building the patch + /// with `--split-debug-info`. + static List splitDebugInfoArgs(String? splitDebugInfoPath) => + splitDebugInfoPath != null + ? [ + '--dwarf-stack-traces', + '--resolve-dwarf-paths', + '--save-debugging-info=${saveDebuggingInfoPath(splitDebugInfoPath)}', + ] + : []; + + /// The doctor validators that should run before this Apple patch. + List get applePlatformValidators; + + @override + Future assertPreconditions() async { + try { + await shorebirdValidator.validatePreconditions( + checkShorebirdInitialized: true, + checkUserIsAuthenticated: true, + validators: applePlatformValidators, + supportedOperatingSystems: {Platform.macOS}, + ); + } on PreconditionFailedException catch (error) { + throw ProcessExit(error.exitCode.code); + } + } + + @override + Future updatedCreatePatchMetadata( + CreatePatchMetadata metadata, + ) async => metadata.copyWith( + linkPercentage: linkPercentage, + linkMetadata: linkMetadata, + environment: metadata.environment.copyWith( + xcodeVersion: await xcodeBuild.version(), + ), + ); + + /// Linker output (link map / version info) attached to patch metadata. + /// Returns `null` if the platform does not use a linker or if the linking + /// step has not yet been run. + Json? get linkMetadata => null; +} + +/// Adds Podfile.lock-based native-change detection to [ApplePatcherMixin]. +/// Implemented by iOS and macOS patchers; the iOS framework patcher doesn't +/// emit a Podfile.lock. +mixin ApplePodfileLockPatcherMixin on Patcher, ApplePatcherMixin { + /// SHA-256 of the local Podfile.lock for this Apple platform, or null if + /// no Podfile.lock exists. + String? get localPodfileLockHash; + + /// Project-relative path of the Podfile.lock surfaced in the warning. + String get podfileLockRelativePath; + + @override + Future assertUnpatchableDiffs({ + required ReleaseArtifact releaseArtifact, + required File releaseArchive, + required File patchArchive, + }) async { + // Check for diffs without warning about native changes, as Xcode builds + // can be nondeterministic. So we still have some hope of alerting users of + // unpatchable native changes, we compare the Podfile.lock hash between the + // patch and the release. + final diffStatus = await patchDiffChecker + .confirmUnpatchableDiffsIfNecessary( + localArchive: patchArchive, + releaseArchive: releaseArchive, + archiveDiffer: const AppleArchiveDiffer(), + allowAssetChanges: allowAssetDiffs, + allowNativeChanges: allowNativeDiffs, + confirmNativeChanges: false, + ); + + if (!diffStatus.hasNativeChanges) return diffStatus; + + if (releaseArtifact.podfileLockHash != null && + localPodfileLockHash != releaseArtifact.podfileLockHash) { + logger.warn( + ''' +Your $podfileLockRelativePath is different from the one used to build the release. +This may indicate that the patch contains native changes, which cannot be applied with a patch. Proceeding may result in unexpected behavior or crashes.''', + ); + + if (!allowNativeDiffs) { + if (!shorebirdEnv.canAcceptUserInput) { + throw UnpatchableChangeException(); + } + + if (!logger.confirm('Continue anyway?', hint: allowNativeDiffsHint)) { + throw UserCancelledException(); + } + } + } + + return diffStatus; + } +} diff --git a/packages/shorebird_cli/lib/src/commands/patch/ios_framework_patcher.dart b/packages/shorebird_cli/lib/src/commands/patch/ios_framework_patcher.dart index 5caf9dd0..99514019 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/ios_framework_patcher.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/ios_framework_patcher.dart @@ -4,32 +4,31 @@ import 'package:crypto/crypto.dart'; import 'package:mason_logger/mason_logger.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; import 'package:shorebird_cli/src/archive/directory_archive.dart'; import 'package:shorebird_cli/src/archive_analysis/apple_archive_differ.dart'; import 'package:shorebird_cli/src/artifact_builder/artifact_builder.dart'; import 'package:shorebird_cli/src/artifact_manager.dart'; import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; +import 'package:shorebird_cli/src/commands/patch/apple_patcher_mixin.dart'; import 'package:shorebird_cli/src/commands/patch/patch.dart'; import 'package:shorebird_cli/src/doctor.dart'; import 'package:shorebird_cli/src/executables/aot_tools.dart'; -import 'package:shorebird_cli/src/executables/xcodebuild.dart'; import 'package:shorebird_cli/src/extensions/arg_results.dart'; import 'package:shorebird_cli/src/logging/logging.dart'; -import 'package:shorebird_cli/src/metadata/metadata.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_artifacts.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_cli/src/validators/validators.dart'; import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; +import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart'; /// {@template ios_framework_patcher} /// Functions to patch an iOS Framework release. /// {@endtemplate} -class IosFrameworkPatcher extends Patcher { +class IosFrameworkPatcher extends Patcher with ApplePatcherMixin { /// {@macro ios_framework_patcher} IosFrameworkPatcher({ required super.argResults, @@ -56,31 +55,23 @@ class IosFrameworkPatcher extends Patcher { @override ReleaseType get releaseType => ReleaseType.iosFramework; + @override + List get applePlatformValidators => doctor.iosCommandValidators; + /// The last build's link metadata. @visibleForTesting - Map? lastBuildLinkMetadata; + Json? lastBuildLinkMetadata; @override double? get linkPercentage => lastBuildLinkPercentage; + @override + Json? get linkMetadata => lastBuildLinkMetadata; + /// The last build link percentage. @visibleForTesting double? lastBuildLinkPercentage; - @override - Future assertPreconditions() async { - try { - await shorebirdValidator.validatePreconditions( - checkUserIsAuthenticated: true, - checkShorebirdInitialized: true, - validators: doctor.iosCommandValidators, - supportedOperatingSystems: {Platform.macOS}, - ); - } on PreconditionFailedException catch (e) { - throw ProcessExit(e.exitCode.code); - } - } - @override Future assertArgsAreValid() async { if (!argResults.wasParsed('release-version')) { @@ -118,7 +109,7 @@ class IosFrameworkPatcher extends Patcher { outFilePath: _aotOutputPath, genSnapshotArtifact: ShorebirdArtifact.genSnapshotIos, additionalArgs: [ - ...IosPatcher.splitDebugInfoArgs(splitDebugInfoPath), + ...ApplePatcherMixin.splitDebugInfoArgs(splitDebugInfoPath), ...obfuscationGenSnapshotArgs, ], ); @@ -180,7 +171,7 @@ class IosFrameworkPatcher extends Patcher { kernelFile: File(_appDillCopyPath), releaseArtifact: releaseArtifactFile, splitDebugInfoArgs: [ - ...IosPatcher.splitDebugInfoArgs(splitDebugInfoPath), + ...ApplePatcherMixin.splitDebugInfoArgs(splitDebugInfoPath), ...obfuscationGenSnapshotArgs, ], aotOutputFile: File(_aotOutputPath), @@ -253,15 +244,4 @@ class IosFrameworkPatcher extends Patcher { 'Release version must be specified using --release-version.', ); } - - @override - Future updatedCreatePatchMetadata( - CreatePatchMetadata metadata, - ) async => metadata.copyWith( - linkPercentage: lastBuildLinkPercentage, - linkMetadata: lastBuildLinkMetadata, - environment: metadata.environment.copyWith( - xcodeVersion: await xcodeBuild.version(), - ), - ); } diff --git a/packages/shorebird_cli/lib/src/commands/patch/ios_patcher.dart b/packages/shorebird_cli/lib/src/commands/patch/ios_patcher.dart index 9c3bd8b1..43ebe0ec 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/ios_patcher.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/ios_patcher.dart @@ -5,35 +5,33 @@ import 'package:crypto/crypto.dart'; import 'package:mason_logger/mason_logger.dart'; import 'package:meta/meta.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/archive_analysis/archive_analysis.dart'; import 'package:shorebird_cli/src/artifact_builder/artifact_builder.dart'; import 'package:shorebird_cli/src/artifact_manager.dart'; import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; +import 'package:shorebird_cli/src/commands/patch/apple_patcher_mixin.dart'; import 'package:shorebird_cli/src/commands/patch/patcher.dart'; 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/extensions/arg_results.dart'; import 'package:shorebird_cli/src/logging/logging.dart'; -import 'package:shorebird_cli/src/metadata/metadata.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_artifacts.dart'; import 'package:shorebird_cli/src/shorebird_documentation.dart'; import 'package:shorebird_cli/src/shorebird_env.dart'; import 'package:shorebird_cli/src/shorebird_flutter.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_cli/src/validators/validators.dart'; import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart'; /// {@template ios_patcher} /// Functions to create an iOS patch. /// {@endtemplate} -class IosPatcher extends Patcher { +class IosPatcher extends Patcher + with ApplePatcherMixin, ApplePodfileLockPatcherMixin { /// {@macro ios_patcher} IosPatcher({ required super.argResults, @@ -51,26 +49,6 @@ class IosPatcher extends Patcher { String get _appDillCopyPath => p.join(shorebirdEnv.buildDirectory.path, 'app.dill'); - /// The name of the split debug info file when the target is iOS. - static const splitDebugInfoFileName = 'app.ios-arm64.symbols'; - - /// The additional gen_snapshot arguments to use when building the patch with - /// `--split-debug-info`. - static List splitDebugInfoArgs(String? splitDebugInfoPath) { - return splitDebugInfoPath != null - ? [ - '--dwarf-stack-traces', - '--resolve-dwarf-paths', - '''--save-debugging-info=${saveDebuggingInfoPath(splitDebugInfoPath)}''', - ] - : []; - } - - /// The path to save the split debug info file. - static String saveDebuggingInfoPath(String directory) { - return p.join(p.absolute(directory), splitDebugInfoFileName); - } - /// The last build's link percentage. @visibleForTesting double? lastBuildLinkPercentage; @@ -82,6 +60,9 @@ class IosPatcher extends Patcher { @override double? get linkPercentage => lastBuildLinkPercentage; + @override + Json? get linkMetadata => lastBuildLinkMetadata; + @override ReleaseType get releaseType => ReleaseType.ios; @@ -92,18 +73,13 @@ class IosPatcher extends Patcher { String? get supplementaryReleaseArtifactArch => 'ios_supplement'; @override - Future assertPreconditions() async { - try { - await shorebirdValidator.validatePreconditions( - checkShorebirdInitialized: true, - checkUserIsAuthenticated: true, - validators: doctor.iosCommandValidators, - supportedOperatingSystems: {Platform.macOS}, - ); - } on PreconditionFailedException catch (error) { - throw ProcessExit(error.exitCode.code); - } - } + List get applePlatformValidators => doctor.iosCommandValidators; + + @override + String? get localPodfileLockHash => shorebirdEnv.iosPodfileLockHash; + + @override + String get podfileLockRelativePath => 'ios/Podfile.lock'; @override Future assertArgsAreValid() async { @@ -120,56 +96,6 @@ class IosPatcher extends Patcher { } } - @override - Future assertUnpatchableDiffs({ - required ReleaseArtifact releaseArtifact, - required File releaseArchive, - required File patchArchive, - }) async { - // Check for diffs without warning about native changes, as Xcode builds - // can be nondeterministic. So we still have some hope of alerting users of - // unpatchable native changes, we compare the Podfile.lock hash between the - // patch and the release. - final diffStatus = await patchDiffChecker - .confirmUnpatchableDiffsIfNecessary( - localArchive: patchArchive, - releaseArchive: releaseArchive, - archiveDiffer: const AppleArchiveDiffer(), - allowAssetChanges: allowAssetDiffs, - allowNativeChanges: allowNativeDiffs, - confirmNativeChanges: false, - ); - - if (!diffStatus.hasNativeChanges) { - return diffStatus; - } - - final podfileLockHash = shorebirdEnv.iosPodfileLockHash; - if (releaseArtifact.podfileLockHash != null && - podfileLockHash != releaseArtifact.podfileLockHash) { - logger.warn( - ''' -Your ios/Podfile.lock is different from the one used to build the release. -This may indicate that the patch contains native changes, which cannot be applied with a patch. Proceeding may result in unexpected behavior or crashes.''', - ); - - if (!allowNativeDiffs) { - if (!shorebirdEnv.canAcceptUserInput) { - throw UnpatchableChangeException(); - } - - if (!logger.confirm( - 'Continue anyway?', - hint: allowNativeDiffsHint, - )) { - throw UserCancelledException(); - } - } - } - - return diffStatus; - } - @override Future buildPatchArtifact({String? releaseVersion}) async { final shouldCodesign = argResults['codesign'] == true; @@ -210,7 +136,7 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}'''); outFilePath: _aotOutputPath, genSnapshotArtifact: ShorebirdArtifact.genSnapshotIos, additionalArgs: [ - ...splitDebugInfoArgs(splitDebugInfoPath), + ...ApplePatcherMixin.splitDebugInfoArgs(splitDebugInfoPath), ...obfuscationGenSnapshotArgs, ], ); @@ -275,7 +201,7 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}'''); kernelFile: File(_appDillCopyPath), releaseArtifact: releaseArtifactFile, splitDebugInfoArgs: [ - ...splitDebugInfoArgs(splitDebugInfoPath), + ...ApplePatcherMixin.splitDebugInfoArgs(splitDebugInfoPath), ...obfuscationGenSnapshotArgs, ], aotOutputFile: File(_aotOutputPath), @@ -365,15 +291,4 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}'''); throw ProcessExit(ExitCode.software.code); } } - - @override - Future updatedCreatePatchMetadata( - CreatePatchMetadata metadata, - ) async => metadata.copyWith( - linkPercentage: lastBuildLinkPercentage, - linkMetadata: lastBuildLinkMetadata, - environment: metadata.environment.copyWith( - xcodeVersion: await xcodeBuild.version(), - ), - ); } diff --git a/packages/shorebird_cli/lib/src/commands/patch/macos_patcher.dart b/packages/shorebird_cli/lib/src/commands/patch/macos_patcher.dart index 48794b2f..88cde955 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/macos_patcher.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/macos_patcher.dart @@ -3,32 +3,29 @@ import 'dart:io'; import 'package:crypto/crypto.dart'; 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_analysis/apple_archive_differ.dart'; import 'package:shorebird_cli/src/artifact_builder/artifact_builder.dart'; import 'package:shorebird_cli/src/artifact_manager.dart'; import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; +import 'package:shorebird_cli/src/commands/patch/apple_patcher_mixin.dart'; import 'package:shorebird_cli/src/commands/patch/patch.dart'; import 'package:shorebird_cli/src/doctor.dart'; import 'package:shorebird_cli/src/executables/executables.dart'; import 'package:shorebird_cli/src/extensions/arg_results.dart'; import 'package:shorebird_cli/src/logging/shorebird_logger.dart'; -import 'package:shorebird_cli/src/metadata/metadata.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_artifacts.dart'; import 'package:shorebird_cli/src/shorebird_documentation.dart'; import 'package:shorebird_cli/src/shorebird_env.dart'; import 'package:shorebird_cli/src/shorebird_flutter.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'; +import 'package:shorebird_cli/src/validators/validators.dart'; /// {@template macos_patcher} /// Functions to create and apply patches to a macOS release. /// {@endtemplate} -class MacosPatcher extends Patcher { +class MacosPatcher extends Patcher + with ApplePatcherMixin, ApplePodfileLockPatcherMixin { /// {@macro macos_patcher} MacosPatcher({ required super.argParser, @@ -61,70 +58,13 @@ class MacosPatcher extends Patcher { String? get supplementaryReleaseArtifactArch => 'macos_supplement'; @override - Future assertPreconditions() async { - try { - await shorebirdValidator.validatePreconditions( - checkShorebirdInitialized: true, - checkUserIsAuthenticated: true, - validators: doctor.macosCommandValidators, - supportedOperatingSystems: {Platform.macOS}, - ); - } on PreconditionFailedException catch (error) { - throw ProcessExit(error.exitCode.code); - } - } + List get applePlatformValidators => doctor.macosCommandValidators; - // TODO(bryanoltman): this is a direct copy of IosPatcher's implementation. We - // should consolidate this and other copied code. @override - Future assertUnpatchableDiffs({ - required ReleaseArtifact releaseArtifact, - required File releaseArchive, - required File patchArchive, - }) async { - // Check for diffs without warning about native changes, as Xcode builds - // can be nondeterministic. So we still have some hope of alerting users of - // unpatchable native changes, we compare the Podfile.lock hash between the - // patch and the release. - final diffStatus = await patchDiffChecker - .confirmUnpatchableDiffsIfNecessary( - localArchive: patchArchive, - releaseArchive: releaseArchive, - archiveDiffer: const AppleArchiveDiffer(), - allowAssetChanges: allowAssetDiffs, - allowNativeChanges: allowNativeDiffs, - confirmNativeChanges: false, - ); + String? get localPodfileLockHash => shorebirdEnv.macosPodfileLockHash; - if (!diffStatus.hasNativeChanges) { - return diffStatus; - } - - final podfileLockHash = shorebirdEnv.macosPodfileLockHash; - if (releaseArtifact.podfileLockHash != null && - podfileLockHash != releaseArtifact.podfileLockHash) { - logger.warn( - ''' -Your macos/Podfile.lock is different from the one used to build the release. -This may indicate that the patch contains native changes, which cannot be applied with a patch. Proceeding may result in unexpected behavior or crashes.''', - ); - - if (!allowNativeDiffs) { - if (!shorebirdEnv.canAcceptUserInput) { - throw UnpatchableChangeException(); - } - - if (!logger.confirm( - 'Continue anyway?', - hint: allowNativeDiffsHint, - )) { - throw UserCancelledException(); - } - } - } - - return diffStatus; - } + @override + String get podfileLockRelativePath => 'macos/Podfile.lock'; @override Future buildPatchArtifact({String? releaseVersion}) async { @@ -165,7 +105,7 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}'''); outFilePath: _arm64AotOutputPath, genSnapshotArtifact: ShorebirdArtifact.genSnapshotMacosArm64, additionalArgs: [ - ...IosPatcher.splitDebugInfoArgs(splitDebugInfoPath), + ...ApplePatcherMixin.splitDebugInfoArgs(splitDebugInfoPath), ...obfuscationGenSnapshotArgs, ], ); @@ -179,7 +119,7 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}'''); outFilePath: _x64AotOutputPath, genSnapshotArtifact: ShorebirdArtifact.genSnapshotMacosX64, additionalArgs: [ - ...IosPatcher.splitDebugInfoArgs(splitDebugInfoPath), + ...ApplePatcherMixin.splitDebugInfoArgs(splitDebugInfoPath), ...obfuscationGenSnapshotArgs, ], ); @@ -295,13 +235,4 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}'''); throw ProcessExit(ExitCode.software.code); } } - - @override - Future updatedCreatePatchMetadata( - CreatePatchMetadata metadata, - ) async => metadata.copyWith( - environment: metadata.environment.copyWith( - xcodeVersion: await xcodeBuild.version(), - ), - ); } diff --git a/packages/shorebird_cli/lib/src/commands/release/apple_releaser_mixin.dart b/packages/shorebird_cli/lib/src/commands/release/apple_releaser_mixin.dart new file mode 100644 index 00000000..aa9e9d56 --- /dev/null +++ b/packages/shorebird_cli/lib/src/commands/release/apple_releaser_mixin.dart @@ -0,0 +1,57 @@ +import 'package:mason_logger/mason_logger.dart'; +import 'package:platform/platform.dart'; +import 'package:shorebird_cli/src/commands/release/releaser.dart'; +import 'package:shorebird_cli/src/executables/xcodebuild.dart'; +import 'package:shorebird_cli/src/logging/logging.dart'; +import 'package:shorebird_cli/src/metadata/metadata.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_cli/src/validators/validators.dart'; + +/// Shared logic for Apple-platform releasers (iOS, macOS, iOS framework). +/// +/// Concrete releasers supply the platform-specific validators via +/// [applePlatformValidators]; the mixin handles the common preconditions and +/// metadata enrichment that every Apple release performs. +mixin AppleReleaserMixin on Releaser { + /// The doctor validators that should run before this Apple release. + List get applePlatformValidators; + + @override + Future assertPreconditions() async { + try { + await shorebirdValidator.validatePreconditions( + checkUserIsAuthenticated: true, + checkShorebirdInitialized: true, + validators: applePlatformValidators, + supportedOperatingSystems: {Platform.macOS}, + ); + } on PreconditionFailedException catch (e) { + throw ProcessExit(e.exitCode.code); + } + } + + @override + Future updatedReleaseMetadata( + UpdateReleaseMetadata metadata, + ) async => metadata.copyWith( + environment: metadata.environment.copyWith( + xcodeVersion: await xcodeBuild.version(), + ), + ); + + /// Rejects `--release-version`, which is only valid for releases whose + /// version cannot be inferred from the built artifact (aar, ios-framework). + /// Call from `assertArgsAreValid` in iOS/macOS releasers. + void assertReleaseVersionFlagNotProvided() { + if (argResults.wasParsed('release-version')) { + logger.err( + ''' +The "--release-version" flag is only supported for aar and ios-framework releases. + +To change the version of this release, change your app's version in your pubspec.yaml.''', + ); + throw ProcessExit(ExitCode.usage.code); + } + } +} diff --git a/packages/shorebird_cli/lib/src/commands/release/ios_framework_releaser.dart b/packages/shorebird_cli/lib/src/commands/release/ios_framework_releaser.dart index b6d89b01..19fdac9a 100644 --- a/packages/shorebird_cli/lib/src/commands/release/ios_framework_releaser.dart +++ b/packages/shorebird_cli/lib/src/commands/release/ios_framework_releaser.dart @@ -3,27 +3,25 @@ import 'dart:io'; import 'package:io/io.dart'; import 'package:mason_logger/mason_logger.dart'; import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; import 'package:shorebird_cli/src/artifact_builder/artifact_builder.dart'; import 'package:shorebird_cli/src/artifact_manager.dart'; import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; +import 'package:shorebird_cli/src/commands/release/apple_releaser_mixin.dart'; import 'package:shorebird_cli/src/commands/release/releaser.dart'; import 'package:shorebird_cli/src/doctor.dart'; -import 'package:shorebird_cli/src/executables/xcodebuild.dart'; import 'package:shorebird_cli/src/extensions/arg_results.dart'; import 'package:shorebird_cli/src/flutter_version_constraints.dart'; import 'package:shorebird_cli/src/logging/logging.dart'; -import 'package:shorebird_cli/src/metadata/metadata.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_cli/src/validators/validators.dart'; import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; /// {@template ios_framework_releaser} /// Functions to create an iOS framework release. /// {@endtemplate} -class IosFrameworkReleaser extends Releaser { +class IosFrameworkReleaser extends Releaser with AppleReleaserMixin { /// {@macro ios_framework_releaser} IosFrameworkReleaser({ required super.argResults, @@ -48,6 +46,9 @@ class IosFrameworkReleaser extends Releaser { @override String get supplementArtifactArch => 'ios_framework_supplement'; + @override + List get applePlatformValidators => doctor.iosCommandValidators; + @override Future assertArgsAreValid() async { if (!argResults.wasParsed('release-version')) { @@ -61,20 +62,6 @@ class IosFrameworkReleaser extends Releaser { @override Version? get minimumFlutterVersion => minimumSupportedIosFlutterVersion; - @override - Future assertPreconditions() async { - try { - await shorebirdValidator.validatePreconditions( - checkUserIsAuthenticated: true, - checkShorebirdInitialized: true, - supportedOperatingSystems: {Platform.macOS}, - validators: doctor.iosCommandValidators, - ); - } on PreconditionFailedException catch (e) { - throw ProcessExit(e.exitCode.code); - } - } - @override Future buildReleaseArtifacts() async { // Delete the Shorebird supplement directory if it exists. @@ -139,15 +126,6 @@ class IosFrameworkReleaser extends Releaser { await uploadSupplementArtifact(appId: appId, releaseId: release.id); } - @override - Future updatedReleaseMetadata( - UpdateReleaseMetadata metadata, - ) async => metadata.copyWith( - environment: metadata.environment.copyWith( - xcodeVersion: await xcodeBuild.version(), - ), - ); - @override String get postReleaseInstructions { final relativeFrameworkDirectoryPath = p.relative(releaseDirectory.path); diff --git a/packages/shorebird_cli/lib/src/commands/release/ios_releaser.dart b/packages/shorebird_cli/lib/src/commands/release/ios_releaser.dart index db8ddf9f..1174431c 100644 --- a/packages/shorebird_cli/lib/src/commands/release/ios_releaser.dart +++ b/packages/shorebird_cli/lib/src/commands/release/ios_releaser.dart @@ -1,31 +1,28 @@ import 'dart:io'; -import 'package:crypto/crypto.dart'; import 'package:mason_logger/mason_logger.dart'; import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; import 'package:shorebird_cli/src/artifact_builder/artifact_builder.dart'; import 'package:shorebird_cli/src/artifact_manager.dart'; import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; +import 'package:shorebird_cli/src/commands/release/apple_releaser_mixin.dart'; import 'package:shorebird_cli/src/commands/release/releaser.dart'; import 'package:shorebird_cli/src/common_arguments.dart'; import 'package:shorebird_cli/src/doctor.dart'; -import 'package:shorebird_cli/src/executables/xcodebuild.dart'; import 'package:shorebird_cli/src/extensions/arg_results.dart'; import 'package:shorebird_cli/src/flutter_version_constraints.dart'; import 'package:shorebird_cli/src/logging/logging.dart'; -import 'package:shorebird_cli/src/metadata/metadata.dart'; import 'package:shorebird_cli/src/platform/apple/apple.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_cli/src/validators/validators.dart'; import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; /// {@template ios_releaser} /// Functions to build and publish an iOS release. /// {@endtemplate} -class IosReleaser extends Releaser { +class IosReleaser extends Releaser with AppleReleaserMixin { /// {@macro ios_releaser} IosReleaser({ required super.argResults, @@ -49,16 +46,11 @@ class IosReleaser extends Releaser { String get artifactDisplayName => 'iOS app'; @override - Future assertArgsAreValid() async { - if (argResults.wasParsed('release-version')) { - logger.err( - ''' -The "--release-version" flag is only supported for aar and ios-framework releases. + List get applePlatformValidators => doctor.iosCommandValidators; -To change the version of this release, change your app's version in your pubspec.yaml.''', - ); - throw ProcessExit(ExitCode.usage.code); - } + @override + Future assertArgsAreValid() async { + assertReleaseVersionFlagNotProvided(); await assertObfuscationIsSupported(); @@ -78,20 +70,6 @@ To change the version of this release, change your app's version in your pubspec @override Version? get minimumFlutterVersion => minimumSupportedIosFlutterVersion; - @override - Future assertPreconditions() async { - try { - await shorebirdValidator.validatePreconditions( - checkUserIsAuthenticated: true, - checkShorebirdInitialized: true, - validators: doctor.iosCommandValidators, - supportedOperatingSystems: {Platform.macOS}, - ); - } on PreconditionFailedException catch (e) { - throw ProcessExit(e.exitCode.code); - } - } - @override Future buildReleaseArtifacts() async { if (!codesign) { @@ -179,14 +157,6 @@ If left checked, Xcode will rewrite the build number in the uploaded IPA, so the required String appId, }) async { final xcarchiveDirectory = artifactManager.getXcarchiveDirectory()!; - final String? podfileLockHash; - if (shorebirdEnv.iosPodfileLockFile.existsSync()) { - podfileLockHash = sha256 - .convert(shorebirdEnv.iosPodfileLockFile.readAsBytesSync()) - .toString(); - } else { - podfileLockHash = null; - } await codePushClientWrapper.createIosReleaseArtifacts( appId: appId, releaseId: release.id, @@ -195,21 +165,12 @@ If left checked, Xcode will rewrite the build number in the uploaded IPA, so the .getIosAppDirectory(xcarchiveDirectory: xcarchiveDirectory)! .path, isCodesigned: codesign, - podfileLockHash: podfileLockHash, + podfileLockHash: shorebirdEnv.iosPodfileLockHash, ); await uploadSupplementArtifact(appId: appId, releaseId: release.id); } - @override - Future updatedReleaseMetadata( - UpdateReleaseMetadata metadata, - ) async => metadata.copyWith( - environment: metadata.environment.copyWith( - xcodeVersion: await xcodeBuild.version(), - ), - ); - @override String get postReleaseInstructions { final relativeArchivePath = p.relative( diff --git a/packages/shorebird_cli/lib/src/commands/release/macos_releaser.dart b/packages/shorebird_cli/lib/src/commands/release/macos_releaser.dart index d282d6cc..15e4bd1c 100644 --- a/packages/shorebird_cli/lib/src/commands/release/macos_releaser.dart +++ b/packages/shorebird_cli/lib/src/commands/release/macos_releaser.dart @@ -1,29 +1,26 @@ import 'dart:io'; -import 'package:crypto/crypto.dart'; import 'package:mason_logger/mason_logger.dart'; import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; import 'package:shorebird_cli/src/artifact_builder/artifact_builder.dart'; import 'package:shorebird_cli/src/artifact_manager.dart'; import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; +import 'package:shorebird_cli/src/commands/release/apple_releaser_mixin.dart'; import 'package:shorebird_cli/src/commands/release/release.dart'; import 'package:shorebird_cli/src/doctor.dart'; -import 'package:shorebird_cli/src/executables/xcodebuild.dart'; import 'package:shorebird_cli/src/extensions/arg_results.dart'; import 'package:shorebird_cli/src/logging/shorebird_logger.dart'; -import 'package:shorebird_cli/src/metadata/update_release_metadata.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_cli/src/validators/validators.dart'; import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; /// {@template macos_releaser} /// Functions to build and publish a macOS release. /// {@endtemplate} -class MacosReleaser extends Releaser { +class MacosReleaser extends Releaser with AppleReleaserMixin { /// {@macro macos_releaser} MacosReleaser({ required super.argResults, @@ -46,38 +43,18 @@ class MacosReleaser extends Releaser { @override String get artifactDisplayName => 'macOS app'; + @override + List get applePlatformValidators => doctor.macosCommandValidators; + @override Future assertArgsAreValid() async { - if (argResults.wasParsed('release-version')) { - logger.err( - ''' -The "--release-version" flag is only supported for aar and ios-framework releases. - -To change the version of this release, change your app's version in your pubspec.yaml.''', - ); - throw ProcessExit(ExitCode.usage.code); - } - + assertReleaseVersionFlagNotProvided(); await assertObfuscationIsSupported(); } @override Version? get minimumFlutterVersion => minimumSupportedMacosFlutterVersion; - @override - Future assertPreconditions() async { - try { - await shorebirdValidator.validatePreconditions( - checkUserIsAuthenticated: true, - checkShorebirdInitialized: true, - validators: doctor.macosCommandValidators, - supportedOperatingSystems: {Platform.macOS}, - ); - } on PreconditionFailedException catch (e) { - throw ProcessExit(e.exitCode.code); - } - } - @override Future buildReleaseArtifacts() async { if (!codesign) { @@ -148,35 +125,17 @@ To change the version of this release, change your app's version in your pubspec throw ProcessExit(ExitCode.software.code); } - final String? podfileLockHash; - if (shorebirdEnv.macosPodfileLockFile.existsSync()) { - podfileLockHash = sha256 - .convert(shorebirdEnv.macosPodfileLockFile.readAsBytesSync()) - .toString(); - } else { - podfileLockHash = null; - } - await codePushClientWrapper.createMacosReleaseArtifacts( appId: appId, releaseId: release.id, appPath: appDirectory.path, isCodesigned: codesign, - podfileLockHash: podfileLockHash, + podfileLockHash: shorebirdEnv.macosPodfileLockHash, ); await uploadSupplementArtifact(appId: appId, releaseId: release.id); } - @override - Future updatedReleaseMetadata( - UpdateReleaseMetadata metadata, - ) async => metadata.copyWith( - environment: metadata.environment.copyWith( - xcodeVersion: await xcodeBuild.version(), - ), - ); - @override String get postReleaseInstructions => ''' diff --git a/packages/shorebird_cli/test/src/commands/release/ios_releaser_test.dart b/packages/shorebird_cli/test/src/commands/release/ios_releaser_test.dart index 52a4a650..4d0caf68 100644 --- a/packages/shorebird_cli/test/src/commands/release/ios_releaser_test.dart +++ b/packages/shorebird_cli/test/src/commands/release/ios_releaser_test.dart @@ -912,7 +912,6 @@ $body late Directory xcarchiveDirectory; late Directory iosAppDirectory; late Directory supplementDirectory; - late File podfileLockFile; setUp(() { when(() => argResults['codesign']).thenReturn(codesign); @@ -923,15 +922,6 @@ $body xcarchiveDirectory = Directory.systemTemp.createTempSync(); iosAppDirectory = Directory.systemTemp.createTempSync(); supplementDirectory = Directory.systemTemp.createTempSync(); - podfileLockFile = - File( - p.join( - Directory.systemTemp.createTempSync().path, - 'Podfile.lock', - ), - ) - ..createSync(recursive: true) - ..writeAsStringSync(podfileLockContent); when( artifactManager.getXcarchiveDirectory, ).thenReturn(xcarchiveDirectory); @@ -953,7 +943,9 @@ $body podfileLockHash: any(named: 'podfileLockHash'), ), ).thenAnswer((_) async => {}); - when(() => shorebirdEnv.iosPodfileLockFile).thenReturn(podfileLockFile); + when( + () => shorebirdEnv.iosPodfileLockHash, + ).thenReturn('${sha256.convert(utf8.encode(podfileLockContent))}'); }); test('forwards call to codePushClientWrapper', () async { diff --git a/packages/shorebird_cli/test/src/commands/release/macos_releaser_test.dart b/packages/shorebird_cli/test/src/commands/release/macos_releaser_test.dart index 1b0091a7..ff925f7a 100644 --- a/packages/shorebird_cli/test/src/commands/release/macos_releaser_test.dart +++ b/packages/shorebird_cli/test/src/commands/release/macos_releaser_test.dart @@ -744,7 +744,6 @@ To change the version of this release, change your app's version in your pubspec ); late Directory appDirectory; - late File podfileLockFile; setUp(() { when(() => argResults['codesign']).thenReturn(codesign); @@ -754,16 +753,6 @@ To change the version of this release, change your app's version in your pubspec appDirectory = Directory.systemTemp.createTempSync(); - podfileLockFile = - File( - p.join( - Directory.systemTemp.createTempSync().path, - 'Podfile.lock', - ), - ) - ..createSync(recursive: true) - ..writeAsStringSync(podfileLockContent); - when( () => artifactManager.getMacOSAppDirectory(), ).thenReturn(appDirectory); @@ -778,8 +767,8 @@ To change the version of this release, change your app's version in your pubspec ).thenAnswer((_) async => {}); when( - () => shorebirdEnv.macosPodfileLockFile, - ).thenReturn(podfileLockFile); + () => shorebirdEnv.macosPodfileLockHash, + ).thenReturn('${sha256.convert(utf8.encode(podfileLockContent))}'); }); group('when app directory does not exist', () {