refactor(cli): consolidate Apple releaser/patcher duplication into mixins (#3735)

This commit is contained in:
Brandon DeRosier
2026-05-04 16:24:59 -07:00
committed by GitHub
parent ab64594aee
commit 35746dad7e
10 changed files with 256 additions and 361 deletions
@@ -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<String> splitDebugInfoArgs(String? splitDebugInfoPath) =>
splitDebugInfoPath != null
? [
'--dwarf-stack-traces',
'--resolve-dwarf-paths',
'--save-debugging-info=${saveDebuggingInfoPath(splitDebugInfoPath)}',
]
: <String>[];
/// The doctor validators that should run before this Apple patch.
List<Validator> get applePlatformValidators;
@override
Future<void> 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<CreatePatchMetadata> 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<DiffStatus> 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;
}
}
@@ -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<Validator> get applePlatformValidators => doctor.iosCommandValidators;
/// The last build's link metadata.
@visibleForTesting
Map<String, dynamic>? lastBuildLinkMetadata;
Json? lastBuildLinkMetadata;
@override
double? get linkPercentage => lastBuildLinkPercentage;
@override
Json? get linkMetadata => lastBuildLinkMetadata;
/// The last build link percentage.
@visibleForTesting
double? lastBuildLinkPercentage;
@override
Future<void> 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<void> 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<CreatePatchMetadata> updatedCreatePatchMetadata(
CreatePatchMetadata metadata,
) async => metadata.copyWith(
linkPercentage: lastBuildLinkPercentage,
linkMetadata: lastBuildLinkMetadata,
environment: metadata.environment.copyWith(
xcodeVersion: await xcodeBuild.version(),
),
);
}
@@ -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<String> splitDebugInfoArgs(String? splitDebugInfoPath) {
return splitDebugInfoPath != null
? [
'--dwarf-stack-traces',
'--resolve-dwarf-paths',
'''--save-debugging-info=${saveDebuggingInfoPath(splitDebugInfoPath)}''',
]
: <String>[];
}
/// 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<void> 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<Validator> get applePlatformValidators => doctor.iosCommandValidators;
@override
String? get localPodfileLockHash => shorebirdEnv.iosPodfileLockHash;
@override
String get podfileLockRelativePath => 'ios/Podfile.lock';
@override
Future<void> assertArgsAreValid() async {
@@ -120,56 +96,6 @@ class IosPatcher extends Patcher {
}
}
@override
Future<DiffStatus> 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<File> 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<CreatePatchMetadata> updatedCreatePatchMetadata(
CreatePatchMetadata metadata,
) async => metadata.copyWith(
linkPercentage: lastBuildLinkPercentage,
linkMetadata: lastBuildLinkMetadata,
environment: metadata.environment.copyWith(
xcodeVersion: await xcodeBuild.version(),
),
);
}
@@ -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<void> 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<Validator> 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<DiffStatus> 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<File> 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<CreatePatchMetadata> updatedCreatePatchMetadata(
CreatePatchMetadata metadata,
) async => metadata.copyWith(
environment: metadata.environment.copyWith(
xcodeVersion: await xcodeBuild.version(),
),
);
}
@@ -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<Validator> get applePlatformValidators;
@override
Future<void> 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<UpdateReleaseMetadata> 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);
}
}
}
@@ -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<Validator> get applePlatformValidators => doctor.iosCommandValidators;
@override
Future<void> assertArgsAreValid() async {
if (!argResults.wasParsed('release-version')) {
@@ -61,20 +62,6 @@ class IosFrameworkReleaser extends Releaser {
@override
Version? get minimumFlutterVersion => minimumSupportedIosFlutterVersion;
@override
Future<void> 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<FileSystemEntity> 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<UpdateReleaseMetadata> updatedReleaseMetadata(
UpdateReleaseMetadata metadata,
) async => metadata.copyWith(
environment: metadata.environment.copyWith(
xcodeVersion: await xcodeBuild.version(),
),
);
@override
String get postReleaseInstructions {
final relativeFrameworkDirectoryPath = p.relative(releaseDirectory.path);
@@ -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<void> assertArgsAreValid() async {
if (argResults.wasParsed('release-version')) {
logger.err(
'''
The "--release-version" flag is only supported for aar and ios-framework releases.
List<Validator> 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<void> 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<void> 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<FileSystemEntity> 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<UpdateReleaseMetadata> updatedReleaseMetadata(
UpdateReleaseMetadata metadata,
) async => metadata.copyWith(
environment: metadata.environment.copyWith(
xcodeVersion: await xcodeBuild.version(),
),
);
@override
String get postReleaseInstructions {
final relativeArchivePath = p.relative(
@@ -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<Validator> get applePlatformValidators => doctor.macosCommandValidators;
@override
Future<void> 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<void> 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<FileSystemEntity> 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<UpdateReleaseMetadata> updatedReleaseMetadata(
UpdateReleaseMetadata metadata,
) async => metadata.copyWith(
environment: metadata.environment.copyWith(
xcodeVersion: await xcodeBuild.version(),
),
);
@override
String get postReleaseInstructions =>
'''
@@ -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 {
@@ -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', () {