refactor(shorebird_cli): pull ExportOptionsPlist logic into new class (#1804)
This commit is contained in:
@@ -16,6 +16,7 @@ import 'package:shorebird_cli/src/logger.dart';
|
||||
import 'package:shorebird_cli/src/os/os.dart';
|
||||
import 'package:shorebird_cli/src/patch_diff_checker.dart';
|
||||
import 'package:shorebird_cli/src/platform.dart';
|
||||
import 'package:shorebird_cli/src/platform/platform.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_artifacts.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_env.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_flutter.dart';
|
||||
@@ -45,6 +46,7 @@ Future<void> main(List<String> args) async {
|
||||
httpClientRef,
|
||||
idevicesyslogRef,
|
||||
iosDeployRef,
|
||||
iosRef,
|
||||
javaRef,
|
||||
loggerRef,
|
||||
osInterfaceRef,
|
||||
|
||||
@@ -18,9 +18,9 @@ import 'package:shorebird_cli/src/engine_config.dart';
|
||||
import 'package:shorebird_cli/src/executables/executables.dart';
|
||||
import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
||||
import 'package:shorebird_cli/src/formatters/file_size_formatter.dart';
|
||||
import 'package:shorebird_cli/src/ios.dart';
|
||||
import 'package:shorebird_cli/src/logger.dart';
|
||||
import 'package:shorebird_cli/src/patch_diff_checker.dart';
|
||||
import 'package:shorebird_cli/src/platform/platform.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_artifact_mixin.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_artifacts.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_build_mixin.dart';
|
||||
|
||||
@@ -17,9 +17,9 @@ import 'package:shorebird_cli/src/doctor.dart';
|
||||
import 'package:shorebird_cli/src/engine_config.dart';
|
||||
import 'package:shorebird_cli/src/executables/aot_tools.dart';
|
||||
import 'package:shorebird_cli/src/formatters/file_size_formatter.dart';
|
||||
import 'package:shorebird_cli/src/ios.dart';
|
||||
import 'package:shorebird_cli/src/logger.dart';
|
||||
import 'package:shorebird_cli/src/patch_diff_checker.dart';
|
||||
import 'package:shorebird_cli/src/platform/platform.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_artifact_mixin.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_artifacts.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_build_mixin.dart';
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:shorebird_cli/src/config/config.dart';
|
||||
import 'package:shorebird_cli/src/doctor.dart';
|
||||
import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
||||
import 'package:shorebird_cli/src/logger.dart';
|
||||
import 'package:shorebird_cli/src/platform/platform.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_artifact_mixin.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_build_mixin.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_env.dart';
|
||||
@@ -20,9 +21,6 @@ import 'package:shorebird_cli/src/shorebird_validator.dart';
|
||||
import 'package:shorebird_cli/src/validators/validators.dart';
|
||||
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
|
||||
|
||||
const exportMethodArgName = 'export-method';
|
||||
const exportOptionsPlistArgName = 'export-options-plist';
|
||||
|
||||
/// {@template release_ios_command}
|
||||
/// `shorebird release ios`
|
||||
/// Create new app releases for iOS.
|
||||
@@ -132,32 +130,14 @@ make smaller updates to your app.
|
||||
);
|
||||
}
|
||||
|
||||
final exportPlistArg = results[exportOptionsPlistArgName] as String?;
|
||||
if (exportPlistArg != null && results.wasParsed(exportMethodArgName)) {
|
||||
logger.err(
|
||||
'''Cannot specify both --$exportMethodArgName and --$exportOptionsPlistArgName.''',
|
||||
);
|
||||
final File exportOptionsPlist;
|
||||
try {
|
||||
exportOptionsPlist = ios.exportOptionsPlistFromArgs(results);
|
||||
} catch (error) {
|
||||
logger.err('$error');
|
||||
return ExitCode.usage.code;
|
||||
}
|
||||
|
||||
final File? exportOptionsPlist;
|
||||
if (exportPlistArg != null) {
|
||||
exportOptionsPlist = File(exportPlistArg);
|
||||
try {
|
||||
_validateExportOptionsPlist(exportOptionsPlist);
|
||||
} catch (error) {
|
||||
logger.err('$error');
|
||||
return ExitCode.usage.code;
|
||||
}
|
||||
} else if (results.wasParsed(exportMethodArgName)) {
|
||||
final exportMethod = ExportMethod.values.firstWhere(
|
||||
(element) => element.argName == results[exportMethodArgName] as String,
|
||||
);
|
||||
exportOptionsPlist = createExportOptionsPlist(exportMethod: exportMethod);
|
||||
} else {
|
||||
exportOptionsPlist = null;
|
||||
}
|
||||
|
||||
const releasePlatform = ReleasePlatform.ios;
|
||||
final flavor = results.findOption('flavor', argParser: argParser);
|
||||
final target = results.findOption('target', argParser: argParser);
|
||||
@@ -385,25 +365,4 @@ ${styleBold.wrap('Make sure to uncheck "Manage Version and Build Number", or els
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Verifies that [exportOptionsPlistFile] exists and sets
|
||||
/// manageAppVersionAndBuildNumber to false, which prevents Xcode from
|
||||
/// changing the version number out from under us.
|
||||
///
|
||||
/// Throws an exception if validation fails, exits normally if validation
|
||||
/// succeeds.
|
||||
void _validateExportOptionsPlist(File exportOptionsPlistFile) {
|
||||
if (!exportOptionsPlistFile.existsSync()) {
|
||||
throw Exception(
|
||||
'''Export options plist file ${exportOptionsPlistFile.path} does not exist''',
|
||||
);
|
||||
}
|
||||
|
||||
final plist = Plist(file: exportOptionsPlistFile);
|
||||
if (plist.properties['manageAppVersionAndBuildNumber'] != false) {
|
||||
throw Exception(
|
||||
'''Export options plist ${exportOptionsPlistFile.path} does not set manageAppVersionAndBuildNumber to false. This is required for shorebird to work.''',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:shorebird_cli/src/logger.dart';
|
||||
|
||||
void showiOSStatusWarning() {
|
||||
final url = link(
|
||||
uri: Uri.parse('https://docs.shorebird.dev/status'),
|
||||
);
|
||||
logger
|
||||
..warn('iOS support is beta. Some apps may run slower after patching.')
|
||||
..info('See $url for more information.');
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:args/args.dart';
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:scoped/scoped.dart';
|
||||
import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart';
|
||||
import 'package:shorebird_cli/src/logger.dart';
|
||||
|
||||
const exportMethodArgName = 'export-method';
|
||||
const exportOptionsPlistArgName = 'export-options-plist';
|
||||
|
||||
void showiOSStatusWarning() {
|
||||
final url = link(
|
||||
uri: Uri.parse('https://docs.shorebird.dev/status'),
|
||||
);
|
||||
logger
|
||||
..warn('iOS support is beta. Some apps may run slower after patching.')
|
||||
..info('See $url for more information.');
|
||||
}
|
||||
|
||||
/// {@template export_method}
|
||||
/// The method used to export the IPA.
|
||||
/// {@endtemplate}
|
||||
enum ExportMethod {
|
||||
appStore('app-store', 'Upload to the App Store'),
|
||||
adHoc(
|
||||
'ad-hoc',
|
||||
'''
|
||||
Test on designated devices that do not need to be registered with the Apple developer account.
|
||||
Requires a distribution certificate.''',
|
||||
),
|
||||
development(
|
||||
'development',
|
||||
'''Test only on development devices registered with the Apple developer account.''',
|
||||
),
|
||||
enterprise(
|
||||
'enterprise',
|
||||
'Distribute an app registered with the Apple Developer Enterprise Program.',
|
||||
);
|
||||
|
||||
/// {@macro export_method}
|
||||
const ExportMethod(this.argName, this.description);
|
||||
|
||||
/// The command-line argument name for this export method.
|
||||
final String argName;
|
||||
|
||||
/// A description of this method and how/when it should be used.
|
||||
final String description;
|
||||
}
|
||||
|
||||
/// {@template invalid_export_options_plist_exception}
|
||||
/// Thrown when an invalid export options plist is provided.
|
||||
/// {@endtemplate}
|
||||
class InvalidExportOptionsPlistException implements Exception {
|
||||
/// {@macro invalid_export_options_plist_exception}
|
||||
InvalidExportOptionsPlistException(this.message);
|
||||
|
||||
final String message;
|
||||
}
|
||||
|
||||
/// A reference to a [Ios] instance.
|
||||
final iosRef = create(Ios.new);
|
||||
|
||||
/// The [Ios] instance available in the current zone.
|
||||
Ios get ios => read(iosRef);
|
||||
|
||||
class Ios {
|
||||
File exportOptionsPlistFromArgs(ArgResults results) {
|
||||
final exportPlistArg = results[exportOptionsPlistArgName] as String?;
|
||||
if (exportPlistArg != null && results.wasParsed(exportMethodArgName)) {
|
||||
throw ArgumentError(
|
||||
'''Cannot specify both --$exportMethodArgName and --$exportOptionsPlistArgName.''',
|
||||
);
|
||||
}
|
||||
|
||||
final File? exportOptionsPlist;
|
||||
if (exportPlistArg != null) {
|
||||
exportOptionsPlist = File(exportPlistArg);
|
||||
_validateExportOptionsPlist(exportOptionsPlist);
|
||||
return exportOptionsPlist;
|
||||
}
|
||||
|
||||
final ExportMethod? exportMethod;
|
||||
if (results.wasParsed(exportMethodArgName)) {
|
||||
exportMethod = ExportMethod.values.firstWhere(
|
||||
(element) => element.argName == results[exportMethodArgName] as String,
|
||||
);
|
||||
} else {
|
||||
exportMethod = null;
|
||||
}
|
||||
|
||||
return createExportOptionsPlist(
|
||||
exportMethod: exportMethod ?? ExportMethod.appStore,
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates an ExportOptions.plist file, which is used to tell xcodebuild to
|
||||
/// not manage the app version and build number. If we don't do this, then
|
||||
/// xcodebuild will increment the build number if it detects an App Store
|
||||
/// Connect build with the same version and build number. This is a problem
|
||||
/// for us when patching, as patches need to have the same version and build
|
||||
/// number as the release they are patching.
|
||||
/// See
|
||||
/// https://developer.apple.com/forums/thread/690647?answerId=689925022#689925022
|
||||
File createExportOptionsPlist({
|
||||
ExportMethod? exportMethod,
|
||||
}) {
|
||||
exportMethod ??= ExportMethod.appStore;
|
||||
final plistContents = '''
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>manageAppVersionAndBuildNumber</key>
|
||||
<false/>
|
||||
<key>signingStyle</key>
|
||||
<string>automatic</string>
|
||||
<key>uploadBitcode</key>
|
||||
<false/>
|
||||
<key>method</key>
|
||||
<string>${exportMethod.argName}</string>
|
||||
</dict>
|
||||
</plist>
|
||||
''';
|
||||
final tempDir = Directory.systemTemp.createTempSync();
|
||||
final exportPlistFile = File(p.join(tempDir.path, 'ExportOptions.plist'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync(plistContents);
|
||||
return exportPlistFile;
|
||||
}
|
||||
|
||||
/// Verifies that [exportOptionsPlistFile] exists and sets
|
||||
/// manageAppVersionAndBuildNumber to false, which prevents Xcode from
|
||||
/// changing the version number out from under us.
|
||||
///
|
||||
/// Throws an exception if validation fails, exits normally if validation
|
||||
/// succeeds.
|
||||
void _validateExportOptionsPlist(File exportOptionsPlistFile) {
|
||||
if (!exportOptionsPlistFile.existsSync()) {
|
||||
throw FileSystemException(
|
||||
'''Export options plist file ${exportOptionsPlistFile.path} does not exist''',
|
||||
);
|
||||
}
|
||||
|
||||
final plist = Plist(file: exportOptionsPlistFile);
|
||||
if (plist.properties['manageAppVersionAndBuildNumber'] != false) {
|
||||
throw InvalidExportOptionsPlistException(
|
||||
'''Export options plist ${exportOptionsPlistFile.path} does not set manageAppVersionAndBuildNumber to false. This is required for shorebird to work.''',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export 'ios.dart';
|
||||
@@ -2,11 +2,11 @@ import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:shorebird_cli/src/command.dart';
|
||||
import 'package:shorebird_cli/src/engine_config.dart';
|
||||
import 'package:shorebird_cli/src/logger.dart';
|
||||
import 'package:shorebird_cli/src/os/operating_system_interface.dart';
|
||||
import 'package:shorebird_cli/src/platform/platform.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_artifacts.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_process.dart';
|
||||
|
||||
@@ -43,36 +43,6 @@ class BuildException implements Exception {
|
||||
final String message;
|
||||
}
|
||||
|
||||
/// {@template export_method}
|
||||
/// The method used to export the IPA.
|
||||
/// {@endtemplate}
|
||||
enum ExportMethod {
|
||||
appStore('app-store', 'Upload to the App Store'),
|
||||
adHoc(
|
||||
'ad-hoc',
|
||||
'''
|
||||
Test on designated devices that do not need to be registered with the Apple developer account.
|
||||
Requires a distribution certificate.''',
|
||||
),
|
||||
development(
|
||||
'development',
|
||||
'''Test only on development devices registered with the Apple developer account.''',
|
||||
),
|
||||
enterprise(
|
||||
'enterprise',
|
||||
'Distribute an app registered with the Apple Developer Enterprise Program.',
|
||||
);
|
||||
|
||||
/// {@macro export_method}
|
||||
const ExportMethod(this.argName, this.description);
|
||||
|
||||
/// The command-line argument name for this export method.
|
||||
final String argName;
|
||||
|
||||
/// A description of this method and how/when it should be used.
|
||||
final String description;
|
||||
}
|
||||
|
||||
mixin ShorebirdBuildMixin on ShorebirdCommand {
|
||||
// This exists only so tests can get the full list.
|
||||
static const allAndroidArchitectures = <Arch, ArchMetadata>{
|
||||
@@ -223,6 +193,8 @@ mixin ShorebirdBuildMixin on ShorebirdCommand {
|
||||
}) async {
|
||||
return _runShorebirdBuildCommand(() async {
|
||||
const executable = 'flutter';
|
||||
final exportOptionsPlistPath =
|
||||
(exportOptionsPlist ?? ios.createExportOptionsPlist()).path;
|
||||
final arguments = [
|
||||
'build',
|
||||
'ipa',
|
||||
@@ -230,8 +202,7 @@ mixin ShorebirdBuildMixin on ShorebirdCommand {
|
||||
if (flavor != null) '--flavor=$flavor',
|
||||
if (target != null) '--target=$target',
|
||||
if (!codesign) '--no-codesign',
|
||||
if (codesign)
|
||||
'''--export-options-plist=${(exportOptionsPlist ?? createExportOptionsPlist()).path}''',
|
||||
if (codesign) '''--export-options-plist=$exportOptionsPlistPath''',
|
||||
...results.rest,
|
||||
];
|
||||
|
||||
@@ -262,40 +233,6 @@ mixin ShorebirdBuildMixin on ShorebirdCommand {
|
||||
});
|
||||
}
|
||||
|
||||
/// Creates an ExportOptions.plist file, which is used to tell xcodebuild to
|
||||
/// not manage the app version and build number. If we don't do this, then
|
||||
/// xcodebuild will increment the build number if it detects an App Store
|
||||
/// Connect build with the same version and build number. This is a problem
|
||||
/// for us when patching, as patches need to have the same version and build
|
||||
/// number as the release they are patching.
|
||||
/// See
|
||||
/// https://developer.apple.com/forums/thread/690647?answerId=689925022#689925022
|
||||
File createExportOptionsPlist({
|
||||
ExportMethod exportMethod = ExportMethod.appStore,
|
||||
}) {
|
||||
final plistContents = '''
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>manageAppVersionAndBuildNumber</key>
|
||||
<false/>
|
||||
<key>signingStyle</key>
|
||||
<string>automatic</string>
|
||||
<key>uploadBitcode</key>
|
||||
<false/>
|
||||
<key>method</key>
|
||||
<string>${exportMethod.argName}</string>
|
||||
</dict>
|
||||
</plist>
|
||||
''';
|
||||
final tempDir = Directory.systemTemp.createTempSync();
|
||||
final exportPlistFile = File(p.join(tempDir.path, 'ExportOptions.plist'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync(plistContents);
|
||||
return exportPlistFile;
|
||||
}
|
||||
|
||||
/// Builds a release iOS framework (.xcframework) for the current project.
|
||||
Future<void> buildIosFramework() async {
|
||||
return _runShorebirdBuildCommand(() async {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:args/args.dart';
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
@@ -7,6 +9,7 @@ import 'package:shorebird_cli/src/commands/build/build.dart';
|
||||
import 'package:shorebird_cli/src/doctor.dart';
|
||||
import 'package:shorebird_cli/src/logger.dart';
|
||||
import 'package:shorebird_cli/src/os/operating_system_interface.dart';
|
||||
import 'package:shorebird_cli/src/platform/platform.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_env.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_process.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_validator.dart';
|
||||
@@ -20,6 +23,7 @@ void main() {
|
||||
group(BuildIpaCommand, () {
|
||||
late ArgResults argResults;
|
||||
late Doctor doctor;
|
||||
late Ios ios;
|
||||
late Logger logger;
|
||||
late OperatingSystemInterface operatingSystemInterface;
|
||||
late ShorebirdProcessResult buildProcessResult;
|
||||
@@ -35,6 +39,7 @@ void main() {
|
||||
body,
|
||||
values: {
|
||||
doctorRef.overrideWith(() => doctor),
|
||||
iosRef.overrideWith(() => ios),
|
||||
loggerRef.overrideWith(() => logger),
|
||||
osInterfaceRef.overrideWith(() => operatingSystemInterface),
|
||||
processRef.overrideWith(() => shorebirdProcess),
|
||||
@@ -51,6 +56,7 @@ void main() {
|
||||
setUp(() {
|
||||
argResults = MockArgResults();
|
||||
doctor = MockDoctor();
|
||||
ios = MockIos();
|
||||
logger = MockLogger();
|
||||
operatingSystemInterface = MockOperatingSystemInterface();
|
||||
shorebirdProcess = MockShorebirdProcess();
|
||||
@@ -79,6 +85,7 @@ void main() {
|
||||
).thenAnswer((_) async => buildProcessResult);
|
||||
when(() => argResults['codesign']).thenReturn(true);
|
||||
when(() => argResults.rest).thenReturn([]);
|
||||
when(() => ios.createExportOptionsPlist()).thenReturn(File('.'));
|
||||
when(() => logger.progress(any())).thenReturn(MockProgress());
|
||||
when(() => logger.info(any())).thenReturn(null);
|
||||
when(() => operatingSystemInterface.which('flutter'))
|
||||
|
||||
@@ -22,6 +22,7 @@ import 'package:shorebird_cli/src/logger.dart';
|
||||
import 'package:shorebird_cli/src/os/operating_system_interface.dart';
|
||||
import 'package:shorebird_cli/src/patch_diff_checker.dart';
|
||||
import 'package:shorebird_cli/src/platform.dart';
|
||||
import 'package:shorebird_cli/src/platform/ios.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_artifacts.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_build_mixin.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_env.dart';
|
||||
@@ -153,6 +154,7 @@ flutter:
|
||||
late File releaseArtifactFile;
|
||||
late ShorebirdArtifacts shorebirdArtifacts;
|
||||
late Doctor doctor;
|
||||
late Ios ios;
|
||||
late IosArchiveDiffer archiveDiffer;
|
||||
late Progress progress;
|
||||
late Logger logger;
|
||||
@@ -180,6 +182,7 @@ flutter:
|
||||
codePushClientWrapperRef.overrideWith(() => codePushClientWrapper),
|
||||
doctorRef.overrideWith(() => doctor),
|
||||
engineConfigRef.overrideWith(() => engineConfig),
|
||||
iosRef.overrideWith(() => ios),
|
||||
shorebirdArtifactsRef.overrideWith(() => shorebirdArtifacts),
|
||||
loggerRef.overrideWith(() => logger),
|
||||
osInterfaceRef.overrideWith(() => operatingSystemInterface),
|
||||
@@ -271,6 +274,7 @@ flutter:
|
||||
codePushClientWrapper = MockCodePushClientWrapper();
|
||||
doctor = MockDoctor();
|
||||
engineConfig = MockEngineConfig();
|
||||
ios = MockIos();
|
||||
shorebirdArtifacts = MockShorebirdArtifacts();
|
||||
shorebirdRoot = Directory.systemTemp.createTempSync();
|
||||
projectRoot = Directory.systemTemp.createTempSync();
|
||||
@@ -404,6 +408,7 @@ flutter:
|
||||
).thenAnswer((_) async {});
|
||||
when(() => doctor.iosCommandValidators).thenReturn([flutterValidator]);
|
||||
when(() => engineConfig.localEngine).thenReturn(null);
|
||||
when(() => ios.createExportOptionsPlist()).thenReturn(File('.'));
|
||||
when(flutterValidator.validate).thenAnswer((_) async => []);
|
||||
when(() => logger.confirm(any())).thenReturn(true);
|
||||
when(() => logger.progress(any())).thenReturn(progress);
|
||||
|
||||
@@ -6,9 +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:propertylistserialization/propertylistserialization.dart';
|
||||
import 'package:scoped/scoped.dart';
|
||||
import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart';
|
||||
import 'package:shorebird_cli/src/auth/auth.dart';
|
||||
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
||||
import 'package:shorebird_cli/src/commands/commands.dart';
|
||||
@@ -17,7 +15,7 @@ import 'package:shorebird_cli/src/doctor.dart';
|
||||
import 'package:shorebird_cli/src/logger.dart';
|
||||
import 'package:shorebird_cli/src/os/operating_system_interface.dart';
|
||||
import 'package:shorebird_cli/src/platform.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_build_mixin.dart';
|
||||
import 'package:shorebird_cli/src/platform/platform.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_env.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_flutter.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_process.dart';
|
||||
@@ -116,6 +114,7 @@ flutter:
|
||||
late Auth auth;
|
||||
late Progress progress;
|
||||
late Logger logger;
|
||||
late Ios ios;
|
||||
late OperatingSystemInterface operatingSystemInterface;
|
||||
late ShorebirdProcessResult flutterBuildProcessResult;
|
||||
late ShorebirdProcessResult flutterPubGetProcessResult;
|
||||
@@ -133,6 +132,7 @@ flutter:
|
||||
authRef.overrideWith(() => auth),
|
||||
codePushClientWrapperRef.overrideWith(() => codePushClientWrapper),
|
||||
doctorRef.overrideWith(() => doctor),
|
||||
iosRef.overrideWith(() => ios),
|
||||
loggerRef.overrideWith(() => logger),
|
||||
osInterfaceRef.overrideWith(() => operatingSystemInterface),
|
||||
platformRef.overrideWith(() => platform),
|
||||
@@ -179,6 +179,7 @@ flutter:
|
||||
}
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(File(''));
|
||||
registerFallbackValue(ReleasePlatform.ios);
|
||||
registerFallbackValue(ReleaseStatus.draft);
|
||||
registerFallbackValue(FakeRelease());
|
||||
@@ -196,6 +197,7 @@ flutter:
|
||||
operatingSystemInterface = MockOperatingSystemInterface();
|
||||
progress = MockProgress();
|
||||
logger = MockLogger();
|
||||
ios = MockIos();
|
||||
flutterBuildProcessResult = MockProcessResult();
|
||||
flutterPubGetProcessResult = MockProcessResult();
|
||||
flutterValidator = MockShorebirdFlutterValidator();
|
||||
@@ -247,20 +249,17 @@ flutter:
|
||||
when(() => argResults['arch']).thenReturn(arch);
|
||||
when(() => argResults['codesign']).thenReturn(true);
|
||||
when(() => argResults['platform']).thenReturn(releasePlatform);
|
||||
when(() => argResults['export-options-plist']).thenReturn(null);
|
||||
// This is the default value in ReleaseIosCommand.
|
||||
when(() => argResults['export-method']).thenReturn(
|
||||
ExportMethod.appStore.argName,
|
||||
);
|
||||
when(() => argResults.rest).thenReturn([]);
|
||||
when(() => argResults.wasParsed(any())).thenReturn(true);
|
||||
when(() => argResults.wasParsed('export-method')).thenReturn(false);
|
||||
when(() => auth.isAuthenticated).thenReturn(true);
|
||||
when(() => logger.progress(any())).thenReturn(progress);
|
||||
when(() => logger.confirm(any())).thenReturn(true);
|
||||
when(
|
||||
() => logger.prompt(any(), defaultValue: any(named: 'defaultValue')),
|
||||
).thenReturn(version);
|
||||
when(() => ios.exportOptionsPlistFromArgs(argResults)).thenReturn(
|
||||
File('.'),
|
||||
);
|
||||
when(
|
||||
() => operatingSystemInterface.which('flutter'),
|
||||
).thenReturn('/path/to/flutter');
|
||||
@@ -556,12 +555,10 @@ flutter:
|
||||
});
|
||||
});
|
||||
|
||||
group('when both export-method and export-options-plist are provided', () {
|
||||
group('when exportOptionsPlistFromArgs throws exception', () {
|
||||
setUp(() {
|
||||
when(() => argResults.wasParsed(exportMethodArgName)).thenReturn(true);
|
||||
when(
|
||||
() => argResults[exportOptionsPlistArgName],
|
||||
).thenReturn('/path/to/export.plist');
|
||||
when(() => ios.exportOptionsPlistFromArgs(argResults))
|
||||
.thenThrow(ArgumentError('bad args'));
|
||||
});
|
||||
|
||||
test('logs error and exits with usage code', () async {
|
||||
@@ -569,130 +566,7 @@ flutter:
|
||||
final exitCode = await runWithOverrides(command.run);
|
||||
|
||||
expect(exitCode, equals(ExitCode.usage.code));
|
||||
verify(
|
||||
() => logger.err(
|
||||
'Cannot specify both --export-method and --export-options-plist.',
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('when export-method is provided', () {
|
||||
setUp(() {
|
||||
when(() => argResults.wasParsed(exportMethodArgName)).thenReturn(true);
|
||||
when(() => argResults[exportMethodArgName])
|
||||
.thenReturn(ExportMethod.adHoc.argName);
|
||||
when(() => argResults[exportOptionsPlistArgName]).thenReturn(null);
|
||||
});
|
||||
|
||||
test('generates an export options plist with that export method',
|
||||
() async {
|
||||
setUpProjectRoot();
|
||||
await runWithOverrides(command.run);
|
||||
|
||||
final capturedArgs = verify(
|
||||
() => shorebirdProcess.run(
|
||||
'flutter',
|
||||
captureAny(),
|
||||
runInShell: any(named: 'runInShell'),
|
||||
),
|
||||
).captured.first as List<String>;
|
||||
final exportOptionsPlistFile = File(
|
||||
capturedArgs
|
||||
.whereType<String>()
|
||||
.firstWhere((arg) => arg.contains(exportOptionsPlistArgName))
|
||||
.split('=')
|
||||
.last,
|
||||
);
|
||||
final exportOptionsPlist = Plist(file: exportOptionsPlistFile);
|
||||
expect(
|
||||
exportOptionsPlist.properties['method'],
|
||||
ExportMethod.adHoc.argName,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when export-options-plist is provided', () {
|
||||
group('when file does not exist', () {
|
||||
setUp(() {
|
||||
when(() => argResults[exportOptionsPlistArgName])
|
||||
.thenReturn('/does/not/exist');
|
||||
});
|
||||
|
||||
test('exits with usage code', () async {
|
||||
setUpProjectRoot();
|
||||
final exitCode = await runWithOverrides(command.run);
|
||||
|
||||
expect(exitCode, equals(ExitCode.usage.code));
|
||||
verify(
|
||||
() => logger.err(
|
||||
'Exception: Export options plist file /does/not/exist does not exist',
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('when manageAppVersionAndBuildNumber is not set to false', () {
|
||||
const exportPlistContent = '''
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
</dict>
|
||||
</plist>
|
||||
''';
|
||||
|
||||
test('exits with usage code', () async {
|
||||
setUpProjectRoot();
|
||||
final exportPlistFile = File(
|
||||
p.join(projectRoot.path, 'export.plist'),
|
||||
)..writeAsStringSync(exportPlistContent);
|
||||
when(
|
||||
() => argResults[exportOptionsPlistArgName],
|
||||
).thenReturn(exportPlistFile.path);
|
||||
final exitCode = await runWithOverrides(command.run);
|
||||
|
||||
expect(exitCode, equals(ExitCode.usage.code));
|
||||
verify(
|
||||
() => logger.err(
|
||||
'''Exception: Export options plist ${exportPlistFile.path} does not set manageAppVersionAndBuildNumber to false. This is required for shorebird to work.''',
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('when neither export-method nor export-options-plist is provided',
|
||||
() {
|
||||
setUp(() {
|
||||
when(() => argResults.wasParsed(exportMethodArgName)).thenReturn(false);
|
||||
when(() => argResults[exportOptionsPlistArgName]).thenReturn(null);
|
||||
});
|
||||
|
||||
test('generates an export options plist with app-store export method',
|
||||
() async {
|
||||
setUpProjectRoot();
|
||||
await runWithOverrides(command.run);
|
||||
|
||||
final capturedArgs = verify(
|
||||
() => shorebirdProcess.run(
|
||||
'flutter',
|
||||
captureAny(),
|
||||
runInShell: any(named: 'runInShell'),
|
||||
),
|
||||
).captured.first as List<String>;
|
||||
final exportOptionsPlistFile = File(
|
||||
capturedArgs
|
||||
.whereType<String>()
|
||||
.firstWhere((arg) => arg.contains(exportOptionsPlistArgName))
|
||||
.split('=')
|
||||
.last,
|
||||
);
|
||||
final exportOptionsPlist = Plist(file: exportOptionsPlistFile);
|
||||
expect(
|
||||
exportOptionsPlist.properties['method'],
|
||||
ExportMethod.appStore.argName,
|
||||
);
|
||||
verify(() => logger.err('Invalid argument(s): bad args')).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1127,38 +1001,6 @@ flavors:
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('provides appropriate ExportOptions.plist to build ipa command',
|
||||
() async {
|
||||
setUpProjectRoot();
|
||||
|
||||
final exitCode = await runWithOverrides(command.run);
|
||||
|
||||
expect(exitCode, ExitCode.success.code);
|
||||
final capturedArgs = verify(
|
||||
() => shorebirdProcess.run(
|
||||
'flutter',
|
||||
captureAny(),
|
||||
runInShell: any(named: 'runInShell'),
|
||||
),
|
||||
).captured.first as List<String>;
|
||||
final exportOptionsPlistFile = File(
|
||||
capturedArgs
|
||||
.whereType<String>()
|
||||
.firstWhere((arg) => arg.contains('export-options-plist'))
|
||||
.split('=')
|
||||
.last,
|
||||
);
|
||||
expect(exportOptionsPlistFile.existsSync(), isTrue);
|
||||
final exportOptionsPlist =
|
||||
PropertyListSerialization.propertyListWithString(
|
||||
exportOptionsPlistFile.readAsStringSync(),
|
||||
) as Map<String, Object>;
|
||||
expect(exportOptionsPlist['manageAppVersionAndBuildNumber'], isFalse);
|
||||
expect(exportOptionsPlist['signingStyle'], 'automatic');
|
||||
expect(exportOptionsPlist['uploadBitcode'], isFalse);
|
||||
expect(exportOptionsPlist['method'], 'app-store');
|
||||
});
|
||||
|
||||
test('does not provide export options when codesign is false', () async {
|
||||
when(() => argResults['codesign']).thenReturn(false);
|
||||
setUpProjectRoot();
|
||||
|
||||
@@ -22,6 +22,7 @@ import 'package:shorebird_cli/src/executables/devicectl/apple_device.dart';
|
||||
import 'package:shorebird_cli/src/executables/executables.dart';
|
||||
import 'package:shorebird_cli/src/os/os.dart';
|
||||
import 'package:shorebird_cli/src/patch_diff_checker.dart';
|
||||
import 'package:shorebird_cli/src/platform/platform.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_artifacts.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_env.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_flutter.dart';
|
||||
@@ -87,6 +88,8 @@ class MockIOSDeploy extends Mock implements IOSDeploy {}
|
||||
|
||||
class MockIOSink extends Mock implements IOSink {}
|
||||
|
||||
class MockIos extends Mock implements Ios {}
|
||||
|
||||
class MockIosArchiveDiffer extends Mock implements IosArchiveDiffer {}
|
||||
|
||||
class MockJava extends Mock implements Java {}
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:args/args.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:propertylistserialization/propertylistserialization.dart';
|
||||
import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart';
|
||||
import 'package:shorebird_cli/src/platform/platform.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../mocks.dart';
|
||||
|
||||
void main() {
|
||||
group(Ios, () {
|
||||
late Ios ios;
|
||||
|
||||
setUp(() {
|
||||
ios = Ios();
|
||||
});
|
||||
|
||||
group('exportOptionsPlistFromArgs', () {
|
||||
late ArgResults argResults;
|
||||
|
||||
setUp(() {
|
||||
argResults = MockArgResults();
|
||||
|
||||
when(() => argResults.wasParsed(any())).thenReturn(false);
|
||||
});
|
||||
|
||||
group('when both export-method and export-options-plist are provided',
|
||||
() {
|
||||
setUp(() {
|
||||
when(
|
||||
() => argResults.wasParsed(exportMethodArgName),
|
||||
).thenReturn(true);
|
||||
when(
|
||||
() => argResults[exportOptionsPlistArgName],
|
||||
).thenReturn('/path/to/export.plist');
|
||||
});
|
||||
|
||||
test('throws ArgumentError', () {
|
||||
expect(
|
||||
() => ios.exportOptionsPlistFromArgs(argResults),
|
||||
throwsArgumentError,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when export-method is provided', () {
|
||||
setUp(() {
|
||||
when(() => argResults.wasParsed(exportMethodArgName))
|
||||
.thenReturn(true);
|
||||
when(() => argResults[exportMethodArgName])
|
||||
.thenReturn(ExportMethod.adHoc.argName);
|
||||
when(() => argResults[exportOptionsPlistArgName]).thenReturn(null);
|
||||
});
|
||||
|
||||
test('generates an export options plist with that export method',
|
||||
() async {
|
||||
final exportOptionsPlistFile = ios.exportOptionsPlistFromArgs(
|
||||
argResults,
|
||||
);
|
||||
final exportOptionsPlist = Plist(file: exportOptionsPlistFile);
|
||||
expect(
|
||||
exportOptionsPlist.properties['method'],
|
||||
ExportMethod.adHoc.argName,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when export-options-plist is provided', () {
|
||||
group('when file does not exist', () {
|
||||
setUp(() {
|
||||
when(
|
||||
() => argResults[exportOptionsPlistArgName],
|
||||
).thenReturn('/does/not/exist');
|
||||
});
|
||||
|
||||
test('throws a FileSystemException', () async {
|
||||
expect(
|
||||
() => ios.exportOptionsPlistFromArgs(argResults),
|
||||
throwsA(
|
||||
isA<FileSystemException>().having(
|
||||
(e) => e.message,
|
||||
'message',
|
||||
'''Export options plist file /does/not/exist does not exist''',
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when manageAppVersionAndBuildNumber is not set to false', () {
|
||||
const exportPlistContent = '''
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
</dict>
|
||||
</plist>
|
||||
''';
|
||||
|
||||
test('throws InvalidExportOptionsPlistException', () async {
|
||||
final tmpDir = Directory.systemTemp.createTempSync();
|
||||
final exportPlistFile = File(
|
||||
p.join(tmpDir.path, 'export.plist'),
|
||||
)..writeAsStringSync(exportPlistContent);
|
||||
when(
|
||||
() => argResults[exportOptionsPlistArgName],
|
||||
).thenReturn(exportPlistFile.path);
|
||||
expect(
|
||||
() => ios.exportOptionsPlistFromArgs(argResults),
|
||||
throwsA(
|
||||
isA<InvalidExportOptionsPlistException>().having(
|
||||
(e) => e.message,
|
||||
'message',
|
||||
'''Export options plist ${exportPlistFile.path} does not set manageAppVersionAndBuildNumber to false. This is required for shorebird to work.''',
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('when neither export-method nor export-options-plist is provided',
|
||||
() {
|
||||
setUp(() {
|
||||
when(() => argResults.wasParsed(exportMethodArgName))
|
||||
.thenReturn(false);
|
||||
when(() => argResults[exportOptionsPlistArgName]).thenReturn(null);
|
||||
});
|
||||
|
||||
test('generates an export options plist with app-store export method',
|
||||
() async {
|
||||
final exportOptionsPlistFile =
|
||||
ios.exportOptionsPlistFromArgs(argResults);
|
||||
final exportOptionsPlist = Plist(file: exportOptionsPlistFile);
|
||||
expect(
|
||||
exportOptionsPlist.properties['method'],
|
||||
ExportMethod.appStore.argName,
|
||||
);
|
||||
|
||||
final exportOptionsPlistMap =
|
||||
PropertyListSerialization.propertyListWithString(
|
||||
exportOptionsPlistFile.readAsStringSync(),
|
||||
) as Map<String, Object>;
|
||||
expect(
|
||||
exportOptionsPlistMap['manageAppVersionAndBuildNumber'],
|
||||
isFalse,
|
||||
);
|
||||
expect(exportOptionsPlistMap['signingStyle'], 'automatic');
|
||||
expect(exportOptionsPlistMap['uploadBitcode'], isFalse);
|
||||
expect(exportOptionsPlistMap['method'], 'app-store');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user