feat(shorebird_cli): add ability to release with --no-codesign (#1267)
Co-authored-by: Felix Angelov <felix@shorebird.dev>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export 'directory_archive.dart';
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:io/io.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:shorebird_cli/src/third_party/flutter_tools/lib/flutter_tools.dart';
|
||||
|
||||
extension DirectoryArchive on Directory {
|
||||
/// Copies this directory to a temporary directory and zips it.
|
||||
Future<File> zipToTempFile() async {
|
||||
final tempDir = await Directory.systemTemp.createTemp();
|
||||
final outFile = File(p.join(tempDir.path, '${p.basename(path)}.zip'));
|
||||
await Isolate.run(() {
|
||||
copyPathSync(path, tempDir.path);
|
||||
ZipFileEncoder().zipDirectory(tempDir, filename: outFile.path);
|
||||
});
|
||||
return outFile;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export 'android_archive_differ.dart';
|
||||
export 'file_set_diff.dart';
|
||||
export 'ios_archive_differ.dart';
|
||||
export 'ipa.dart';
|
||||
export 'plist.dart';
|
||||
|
||||
@@ -20,11 +20,13 @@ import 'package:shorebird_cli/src/archive_analysis/file_set_diff.dart';
|
||||
class IosArchiveDiffer extends ArchiveDiffer {
|
||||
String _hash(List<int> bytes) => sha256.convert(bytes).toString();
|
||||
|
||||
static const binaryFiles = {
|
||||
'App.framework/App',
|
||||
'Flutter.framework/Flutter',
|
||||
static final binaryFilePatterns = {
|
||||
RegExp(r'App.framework/App$'),
|
||||
RegExp(r'Flutter.framework/Flutter$'),
|
||||
};
|
||||
static RegExp appRegex = RegExp(r'^Payload/[\w\-. ]+.app/[\w\- ]+$');
|
||||
static RegExp appRegex = RegExp(
|
||||
r'^Products/Applications/[\w\-. ]+.app/[\w\- ]+$',
|
||||
);
|
||||
|
||||
/// Files that have been added, removed, or that have changed between the
|
||||
/// archives at the two provided paths. This method will also unisgn mach-o
|
||||
@@ -73,8 +75,8 @@ class IosArchiveDiffer extends ArchiveDiffer {
|
||||
.where((file) => file.isFile)
|
||||
.where(
|
||||
(file) =>
|
||||
file.name.endsWith('App.framework/App') ||
|
||||
file.name.endsWith('Flutter.framework/Flutter') ||
|
||||
binaryFilePatterns
|
||||
.any((pattern) => pattern.hasMatch(file.name)) ||
|
||||
appRegex.hasMatch(file.name),
|
||||
)
|
||||
.toList();
|
||||
@@ -84,7 +86,7 @@ class IosArchiveDiffer extends ArchiveDiffer {
|
||||
return ZipDecoder()
|
||||
.decodeBuffer(InputFileStream(archivePath))
|
||||
.files
|
||||
.where((file) => file.isFile && p.extension(file.name) == '.car')
|
||||
.where((file) => file.isFile && p.basename(file.name) == 'Assets.car')
|
||||
.toList();
|
||||
}
|
||||
|
||||
@@ -102,8 +104,7 @@ class IosArchiveDiffer extends ArchiveDiffer {
|
||||
}
|
||||
|
||||
final outFile = File(outPath);
|
||||
final hash = _hash(outFile.readAsBytesSync());
|
||||
return hash;
|
||||
return _hash(outFile.readAsBytesSync());
|
||||
}
|
||||
|
||||
/// Uses assetutil to write a json description of a .car file to disk and
|
||||
@@ -148,7 +149,7 @@ class IosArchiveDiffer extends ArchiveDiffer {
|
||||
/// The flutter_assets directory contains the assets listed in the assets
|
||||
/// section of the pubspec.yaml file.
|
||||
/// Assets.car is the compiled asset catalog(s) (.xcassets files).
|
||||
return p.extension(filePath) == '.car' ||
|
||||
return p.basename(filePath) == 'Assets.car' ||
|
||||
p.split(filePath).contains('flutter_assets');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:propertylistserialization/propertylistserialization.dart';
|
||||
|
||||
/// {@template ipa_reader}
|
||||
/// Wraps the [Ipa] class to make it easier to test.
|
||||
/// {@endtemplate}
|
||||
class IpaReader {
|
||||
/// {@macro ipa_reader}
|
||||
Ipa read(String path) => Ipa(path: path);
|
||||
}
|
||||
|
||||
/// {@template ipa}
|
||||
/// A class that represents an iOS IPA file.
|
||||
/// {@endtemplate}
|
||||
class Ipa {
|
||||
/// {@macro ipa}
|
||||
Ipa({
|
||||
required String path,
|
||||
}) : _ipaFile = File(path);
|
||||
|
||||
/// This key is a user-visible string for the version of the bundle. The
|
||||
/// required format is three period-separated integers, such as 10.14.1. The
|
||||
/// string can only contain numeric characters (0-9) and periods.
|
||||
///
|
||||
/// See https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring
|
||||
static const releaseVersionKey = 'CFBundleShortVersionString';
|
||||
|
||||
/// The version of the build that identifies an iteration of the bundle.
|
||||
///
|
||||
/// See https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleversion
|
||||
static const buildNumberKey = 'CFBundleVersion';
|
||||
|
||||
final File _ipaFile;
|
||||
|
||||
/// The version number of the IPA, as derived from the app's Info.plist.
|
||||
String get versionNumber {
|
||||
final plist = _getPlist();
|
||||
if (plist == null) {
|
||||
throw Exception('Could not find Info.plist');
|
||||
}
|
||||
|
||||
final releaseVersion = plist[releaseVersionKey] as String?;
|
||||
final buildNumber = plist[buildNumberKey] as String?;
|
||||
if (releaseVersion == null) {
|
||||
throw Exception('Could not determine release version');
|
||||
}
|
||||
|
||||
return buildNumber == null
|
||||
? releaseVersion
|
||||
: '$releaseVersion+$buildNumber';
|
||||
}
|
||||
|
||||
Map<String, Object>? _getPlist() {
|
||||
final plistPathRegex = RegExp(r'Payload/[\w\-. ]+.app/Info.plist');
|
||||
final plistFile = ZipDecoder()
|
||||
.decodeBuffer(InputFileStream(_ipaFile.path))
|
||||
.files
|
||||
.where((file) {
|
||||
return file.isFile && plistPathRegex.hasMatch(file.name);
|
||||
}).firstOrNull;
|
||||
if (plistFile == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final content = plistFile.content as Uint8List;
|
||||
|
||||
return PropertyListSerialization.propertyListWithData(
|
||||
ByteData.view(content.buffer),
|
||||
) as Map<String, Object>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:propertylistserialization/propertylistserialization.dart';
|
||||
|
||||
class Plist {
|
||||
Plist({required File file}) {
|
||||
properties = PropertyListSerialization.propertyListWithString(
|
||||
file.readAsStringSync(),
|
||||
) as Map<String, Object>;
|
||||
}
|
||||
|
||||
/// This key is a user-visible string for the version of the bundle. The
|
||||
/// required format is three period-separated integers, such as 10.14.1. The
|
||||
/// string can only contain numeric characters (0-9) and periods.
|
||||
///
|
||||
/// See https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring
|
||||
static const releaseVersionKey = 'CFBundleShortVersionString';
|
||||
|
||||
/// The version of the build that identifies an iteration of the bundle.
|
||||
///
|
||||
/// See https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleversion
|
||||
static const buildNumberKey = 'CFBundleVersion';
|
||||
|
||||
/// The Info.plist contained in .xcarchives has the following structure:
|
||||
/// {
|
||||
/// ApplicationProperties: {
|
||||
/// CFBundleShortVersionString: "1.0.0",
|
||||
/// CFBundleVersion: "1",
|
||||
/// },
|
||||
static const applicationPropertiesKey = 'ApplicationProperties';
|
||||
|
||||
late final Map<String, Object> properties;
|
||||
|
||||
String get versionNumber {
|
||||
final applicationProperties =
|
||||
properties[applicationPropertiesKey]! as Map<String, Object>;
|
||||
final releaseVersion = applicationProperties[releaseVersionKey] as String?;
|
||||
final buildNumber = applicationProperties[buildNumberKey] as String?;
|
||||
if (releaseVersion == null) {
|
||||
throw Exception('Could not determine release version');
|
||||
}
|
||||
|
||||
return buildNumber == null
|
||||
? releaseVersion
|
||||
: '$releaseVersion+$buildNumber';
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,12 @@ import 'dart:isolate';
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:io/io.dart' as io;
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:scoped/scoped.dart';
|
||||
import 'package:shorebird_cli/src/archive/directory_archive.dart';
|
||||
import 'package:shorebird_cli/src/auth/auth.dart';
|
||||
import 'package:shorebird_cli/src/logger.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_build_mixin.dart';
|
||||
@@ -503,35 +505,51 @@ aar artifact already exists, continuing...''',
|
||||
createArtifactProgress.complete();
|
||||
}
|
||||
|
||||
/// Uploads a release ipa to the Shorebird server.
|
||||
/// Removes all .dylib files from the given .xcarchive to reduce the size of
|
||||
/// the uploaded artifact.
|
||||
Future<Directory> _thinXcarchive({required String xcarchivePath}) async {
|
||||
final xcarchiveDirectoryName = p.basename(xcarchivePath);
|
||||
final tempDir = Directory.systemTemp.createTempSync();
|
||||
final thinnedArchiveDirectory =
|
||||
Directory(p.join(tempDir.path, xcarchiveDirectoryName));
|
||||
await io.copyPath(xcarchivePath, thinnedArchiveDirectory.path);
|
||||
thinnedArchiveDirectory
|
||||
.listSync(recursive: true)
|
||||
.whereType<File>()
|
||||
.where((file) => p.extension(file.path) == '.dylib')
|
||||
.forEach((file) => file.deleteSync());
|
||||
return thinnedArchiveDirectory;
|
||||
}
|
||||
|
||||
/// Uploads a release .xcarchive and .app to the Shorebird server.
|
||||
Future<void> createIosReleaseArtifacts({
|
||||
required String appId,
|
||||
required int releaseId,
|
||||
required String ipaPath,
|
||||
required String xcarchivePath,
|
||||
required String runnerPath,
|
||||
}) async {
|
||||
final createArtifactProgress = logger.progress('Creating artifacts');
|
||||
final ipaFile = File(ipaPath);
|
||||
final thinnedArchiveDirectory =
|
||||
await _thinXcarchive(xcarchivePath: xcarchivePath);
|
||||
final zippedArchive = await thinnedArchiveDirectory.zipToTempFile();
|
||||
try {
|
||||
await codePushClient.createReleaseArtifact(
|
||||
appId: appId,
|
||||
releaseId: releaseId,
|
||||
artifactPath: ipaPath,
|
||||
arch: 'ipa',
|
||||
artifactPath: zippedArchive.path,
|
||||
arch: 'xcarchive',
|
||||
platform: ReleasePlatform.ios,
|
||||
hash: sha256.convert(await ipaFile.readAsBytes()).toString(),
|
||||
hash: sha256.convert(await zippedArchive.readAsBytes()).toString(),
|
||||
);
|
||||
} catch (error) {
|
||||
_handleErrorAndExit(
|
||||
error,
|
||||
progress: createArtifactProgress,
|
||||
message: 'Error uploading ipa: $error',
|
||||
message: 'Error uploading xcarchive: $error',
|
||||
);
|
||||
}
|
||||
|
||||
final runnerDirectory = Directory(runnerPath);
|
||||
await Isolate.run(() => ZipFileEncoder().zipDirectory(runnerDirectory));
|
||||
final zippedRunner = File('$runnerPath.zip');
|
||||
final zippedRunner = await Directory(runnerPath).zipToTempFile();
|
||||
try {
|
||||
await codePushClient.createReleaseArtifact(
|
||||
appId: appId,
|
||||
|
||||
@@ -30,10 +30,8 @@ class PatchIosCommand extends ShorebirdCommand
|
||||
PatchIosCommand({
|
||||
HashFunction? hashFn,
|
||||
IosArchiveDiffer? archiveDiffer,
|
||||
IpaReader? ipaReader,
|
||||
}) : _hashFn = hashFn ?? ((m) => sha256.convert(m).toString()),
|
||||
_archiveDiffer = archiveDiffer ?? IosArchiveDiffer(),
|
||||
_ipaReader = ipaReader ?? IpaReader() {
|
||||
_archiveDiffer = archiveDiffer ?? IosArchiveDiffer() {
|
||||
argParser
|
||||
..addOption(
|
||||
'target',
|
||||
@@ -44,6 +42,11 @@ class PatchIosCommand extends ShorebirdCommand
|
||||
'flavor',
|
||||
help: 'The product flavor to use when building the app.',
|
||||
)
|
||||
..addFlag(
|
||||
'codesign',
|
||||
help: 'Codesign the application bundle.',
|
||||
defaultsTo: true,
|
||||
)
|
||||
..addFlag(
|
||||
'force',
|
||||
abbr: 'f',
|
||||
@@ -67,7 +70,6 @@ class PatchIosCommand extends ShorebirdCommand
|
||||
|
||||
final HashFunction _hashFn;
|
||||
final IosArchiveDiffer _archiveDiffer;
|
||||
final IpaReader _ipaReader;
|
||||
|
||||
@override
|
||||
Future<int> run() async {
|
||||
@@ -107,30 +109,30 @@ class PatchIosCommand extends ShorebirdCommand
|
||||
return ExitCode.software.code;
|
||||
}
|
||||
|
||||
final detectReleaseVersionProgress = logger.progress(
|
||||
'Detecting release version',
|
||||
final archivePath = p.join(
|
||||
Directory.current.path,
|
||||
'build',
|
||||
'ios',
|
||||
'archive',
|
||||
'Runner.xcarchive',
|
||||
);
|
||||
final String ipaPath;
|
||||
final plistFile = File(p.join(archivePath, 'Info.plist'));
|
||||
if (!plistFile.existsSync()) {
|
||||
logger.err('No Info.plist file found at ${plistFile.path}.');
|
||||
return ExitCode.software.code;
|
||||
}
|
||||
|
||||
final plist = Plist(file: plistFile);
|
||||
final String releaseVersion;
|
||||
try {
|
||||
ipaPath = getIpaPath();
|
||||
releaseVersion = plist.versionNumber;
|
||||
} catch (error) {
|
||||
detectReleaseVersionProgress.fail('Could not find ipa file: $error');
|
||||
return ExitCode.software.code;
|
||||
}
|
||||
try {
|
||||
final ipa = _ipaReader.read(ipaPath);
|
||||
releaseVersion = ipa.versionNumber;
|
||||
detectReleaseVersionProgress.complete(
|
||||
'Detected release version $releaseVersion',
|
||||
);
|
||||
} catch (error) {
|
||||
detectReleaseVersionProgress.fail(
|
||||
'Failed to determine release version: $error',
|
||||
);
|
||||
logger.err('Failed to determine release version: $error');
|
||||
return ExitCode.software.code;
|
||||
}
|
||||
|
||||
logger.info('Detected release version $releaseVersion');
|
||||
|
||||
final release = await codePushClientWrapper.getRelease(
|
||||
appId: appId,
|
||||
releaseVersion: releaseVersion,
|
||||
@@ -169,7 +171,7 @@ Current Flutter Revision: $originalFlutterRevision
|
||||
return ExitCode.software.code;
|
||||
} finally {
|
||||
flutterVersionProgress = logger.progress(
|
||||
'Switching back to original Flutter revision $originalFlutterRevision',
|
||||
'''Switching back to original Flutter revision $originalFlutterRevision''',
|
||||
);
|
||||
await shorebirdFlutter.useRevision(revision: originalFlutterRevision);
|
||||
flutterVersionProgress.complete();
|
||||
@@ -179,13 +181,13 @@ Current Flutter Revision: $originalFlutterRevision
|
||||
final releaseArtifact = await codePushClientWrapper.getReleaseArtifact(
|
||||
appId: appId,
|
||||
releaseId: release.id,
|
||||
arch: 'ipa',
|
||||
arch: 'xcarchive',
|
||||
platform: ReleasePlatform.ios,
|
||||
);
|
||||
|
||||
try {
|
||||
await patchDiffChecker.confirmUnpatchableDiffsIfNecessary(
|
||||
localArtifact: File(ipaPath),
|
||||
await patchDiffChecker.zipAndConfirmUnpatchableDiffsIfNecessary(
|
||||
localArtifactDirectory: Directory(archivePath),
|
||||
releaseArtifactUrl: Uri.parse(releaseArtifact.url),
|
||||
archiveDiffer: _archiveDiffer,
|
||||
force: force,
|
||||
@@ -224,8 +226,6 @@ ${summary.join('\n')}
|
||||
''',
|
||||
);
|
||||
|
||||
// TODO(bryanoltman): check for asset changes
|
||||
|
||||
final needsConfirmation = !force && !shorebirdEnv.isRunningOnCI;
|
||||
if (needsConfirmation) {
|
||||
final confirm = logger.confirm('Would you like to continue?');
|
||||
@@ -260,9 +260,12 @@ ${summary.join('\n')}
|
||||
Future<void> _buildPatch() async {
|
||||
final target = results['target'] as String?;
|
||||
final flavor = results['flavor'] as String?;
|
||||
final shouldCodesign = results['codesign'] == true;
|
||||
final buildProgress = logger.progress('Building patch');
|
||||
try {
|
||||
await buildIpa(flavor: flavor, target: target);
|
||||
// If buildIpa is called with a different codesign value than the release
|
||||
// was, we will erroneously report native diffs.
|
||||
await buildIpa(codesign: shouldCodesign, flavor: flavor, target: target);
|
||||
} on ProcessException catch (error) {
|
||||
buildProgress.fail('Failed to build: ${error.message}');
|
||||
rethrow;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:io' hide Platform;
|
||||
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
@@ -168,18 +167,6 @@ Please re-run the release command for this version or create a new release.''');
|
||||
|
||||
buildProgress.complete();
|
||||
|
||||
const zippedFrameworkFileName =
|
||||
'${ShorebirdArtifactMixin.appXcframeworkName}.zip';
|
||||
final tempDir = Directory.systemTemp.createTempSync();
|
||||
final zippedFrameworkPath = p.join(
|
||||
tempDir.path,
|
||||
zippedFrameworkFileName,
|
||||
);
|
||||
ZipFileEncoder().zipDirectory(
|
||||
Directory(getAppXcframeworkPath()),
|
||||
filename: zippedFrameworkPath,
|
||||
);
|
||||
|
||||
final releaseArtifact = await codePushClientWrapper.getReleaseArtifact(
|
||||
appId: appId,
|
||||
releaseId: release.id,
|
||||
@@ -188,8 +175,8 @@ Please re-run the release command for this version or create a new release.''');
|
||||
);
|
||||
|
||||
try {
|
||||
await patchDiffChecker.confirmUnpatchableDiffsIfNecessary(
|
||||
localArtifact: File(zippedFrameworkPath),
|
||||
await patchDiffChecker.zipAndConfirmUnpatchableDiffsIfNecessary(
|
||||
localArtifactDirectory: Directory(getAppXcframeworkPath()),
|
||||
releaseArtifactUrl: Uri.parse(releaseArtifact.url),
|
||||
archiveDiffer: _archiveDiffer,
|
||||
force: force,
|
||||
|
||||
@@ -23,9 +23,7 @@ import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
|
||||
class ReleaseIosCommand extends ShorebirdCommand
|
||||
with ShorebirdBuildMixin, ShorebirdArtifactMixin {
|
||||
/// {@macro release_ios_command}
|
||||
ReleaseIosCommand({
|
||||
IpaReader? ipaReader,
|
||||
}) : _ipaReader = ipaReader ?? IpaReader() {
|
||||
ReleaseIosCommand() {
|
||||
argParser
|
||||
..addOption(
|
||||
'target',
|
||||
@@ -36,6 +34,11 @@ class ReleaseIosCommand extends ShorebirdCommand
|
||||
'flavor',
|
||||
help: 'The product flavor to use when building the app.',
|
||||
)
|
||||
..addFlag(
|
||||
'codesign',
|
||||
help: 'Codesign the application bundle.',
|
||||
defaultsTo: true,
|
||||
)
|
||||
..addFlag(
|
||||
'force',
|
||||
abbr: 'f',
|
||||
@@ -44,8 +47,6 @@ class ReleaseIosCommand extends ShorebirdCommand
|
||||
);
|
||||
}
|
||||
|
||||
final IpaReader _ipaReader;
|
||||
|
||||
@override
|
||||
String get description => '''
|
||||
Builds and submits your iOS app to Shorebird.
|
||||
@@ -71,6 +72,17 @@ make smaller updates to your app.
|
||||
|
||||
showiOSStatusWarning();
|
||||
|
||||
final codesign = results['codesign'] == true;
|
||||
if (!codesign) {
|
||||
logger
|
||||
..info(
|
||||
'''Building for device with codesigning disabled. You will have to manually codesign before deploying to device.''',
|
||||
)
|
||||
..warn(
|
||||
'''shorebird preview will not work for releases created with "--no-codesign". However, you can still preview your app by signing the generated .xcarchive in Xcode.''',
|
||||
);
|
||||
}
|
||||
|
||||
const releasePlatform = ReleasePlatform.ios;
|
||||
final flavor = results['flavor'] as String?;
|
||||
final target = results['target'] as String?;
|
||||
@@ -80,48 +92,54 @@ make smaller updates to your app.
|
||||
|
||||
final buildProgress = logger.progress('Building release');
|
||||
try {
|
||||
await buildIpa(flavor: flavor, target: target);
|
||||
await buildIpa(codesign: codesign, flavor: flavor, target: target);
|
||||
} on ProcessException catch (error) {
|
||||
buildProgress.fail('Failed to build IPA: ${error.message}');
|
||||
buildProgress.fail('Failed to build: ${error.message}');
|
||||
return ExitCode.software.code;
|
||||
} on BuildException catch (error) {
|
||||
buildProgress.fail('Failed to build IPA');
|
||||
buildProgress.fail('Failed to build');
|
||||
logger.err(error.message);
|
||||
return ExitCode.software.code;
|
||||
}
|
||||
|
||||
buildProgress.complete();
|
||||
|
||||
final releaseVersionProgress = logger.progress('Getting release version');
|
||||
final iosBuildDir = p.join(Directory.current.path, 'build', 'ios');
|
||||
// Ensure the ipa was built
|
||||
final String ipaPath;
|
||||
try {
|
||||
ipaPath = getIpaPath();
|
||||
} catch (error) {
|
||||
releaseVersionProgress.fail('Could not find ipa file: $error');
|
||||
logger.err('Could not find ipa file: $error');
|
||||
return ExitCode.software.code;
|
||||
}
|
||||
|
||||
final runnerPath = p.join(
|
||||
final iosBuildDir = p.join(Directory.current.path, 'build', 'ios');
|
||||
|
||||
final archivePath = p.join(
|
||||
iosBuildDir,
|
||||
'archive',
|
||||
'Runner.xcarchive',
|
||||
);
|
||||
final runnerPath = p.join(
|
||||
archivePath,
|
||||
'Products',
|
||||
'Applications',
|
||||
'Runner.app',
|
||||
);
|
||||
String releaseVersion;
|
||||
try {
|
||||
final ipa = _ipaReader.read(ipaPath);
|
||||
releaseVersion = ipa.versionNumber;
|
||||
} catch (error) {
|
||||
releaseVersionProgress.fail(
|
||||
'Failed to determine release version: $error',
|
||||
);
|
||||
final plistFile = File(p.join(archivePath, 'Info.plist'));
|
||||
if (!plistFile.existsSync()) {
|
||||
logger.err('No Info.plist file found at ${plistFile.path}.');
|
||||
return ExitCode.software.code;
|
||||
}
|
||||
|
||||
releaseVersionProgress.complete();
|
||||
final plist = Plist(file: plistFile);
|
||||
final String releaseVersion;
|
||||
try {
|
||||
releaseVersion = plist.versionNumber;
|
||||
} catch (error) {
|
||||
logger.err('Failed to determine release version: $error');
|
||||
return ExitCode.software.code;
|
||||
}
|
||||
|
||||
final existingRelease = await codePushClientWrapper.maybeGetRelease(
|
||||
appId: appId,
|
||||
@@ -177,12 +195,10 @@ ${summary.join('\n')}
|
||||
);
|
||||
}
|
||||
|
||||
final relativeIpaPath = p.relative(ipaPath);
|
||||
|
||||
await codePushClientWrapper.createIosReleaseArtifacts(
|
||||
appId: app.appId,
|
||||
releaseId: release.id,
|
||||
ipaPath: ipaPath,
|
||||
xcarchivePath: archivePath,
|
||||
runnerPath: runnerPath,
|
||||
);
|
||||
|
||||
@@ -193,9 +209,11 @@ ${summary.join('\n')}
|
||||
status: ReleaseStatus.active,
|
||||
);
|
||||
|
||||
logger
|
||||
..success('\n✅ Published Release!')
|
||||
..info('''
|
||||
logger.success('\n✅ Published Release!');
|
||||
|
||||
if (codesign) {
|
||||
final relativeIpaPath = p.relative(ipaPath);
|
||||
logger.info('''
|
||||
|
||||
Your next step is to upload the ipa to App Store Connect.
|
||||
${lightCyan.wrap(relativeIpaPath)}
|
||||
@@ -205,6 +223,17 @@ To upload to the App Store either:
|
||||
2. Run ${lightCyan.wrap('xcrun altool --upload-app --type ios -f $relativeIpaPath --apiKey your_api_key --apiIssuer your_issuer_id')}.
|
||||
See "man altool" for details about how to authenticate with the App Store Connect API key.
|
||||
''');
|
||||
} else {
|
||||
logger.info('''
|
||||
|
||||
Your next step is to submit the archive at ${lightCyan.wrap(archivePath)} to the App Store using Xcode.
|
||||
|
||||
You can open the archive in Xcode by running:
|
||||
${lightCyan.wrap('open $archivePath')}
|
||||
|
||||
${styleBold.wrap('Make sure to uncheck "Manage Version and Build Number", or else shorebird will not work.')}
|
||||
''');
|
||||
}
|
||||
|
||||
return ExitCode.success.code;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:http/http.dart' as http;
|
||||
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/archive.dart';
|
||||
import 'package:shorebird_cli/src/archive_analysis/archive_differ.dart';
|
||||
import 'package:shorebird_cli/src/http_client/http_client.dart';
|
||||
import 'package:shorebird_cli/src/logger.dart';
|
||||
@@ -35,6 +36,26 @@ class PatchDiffChecker {
|
||||
|
||||
final http.Client _httpClient;
|
||||
|
||||
/// Zips the contents of [localArtifactDirectory] to a temporary file and
|
||||
/// forwards to [confirmUnpatchableDiffsIfNecessary].
|
||||
Future<void> zipAndConfirmUnpatchableDiffsIfNecessary({
|
||||
required Directory localArtifactDirectory,
|
||||
required Uri releaseArtifactUrl,
|
||||
required ArchiveDiffer archiveDiffer,
|
||||
required bool force,
|
||||
}) async {
|
||||
final zipProgress = logger.progress('Compressing archive');
|
||||
final zippedFile = await localArtifactDirectory.zipToTempFile();
|
||||
zipProgress.complete();
|
||||
|
||||
return confirmUnpatchableDiffsIfNecessary(
|
||||
localArtifact: zippedFile,
|
||||
releaseArtifactUrl: releaseArtifactUrl,
|
||||
archiveDiffer: archiveDiffer,
|
||||
force: force,
|
||||
);
|
||||
}
|
||||
|
||||
/// Downloads the release artifact at [releaseArtifactUrl] and checks for
|
||||
/// differences that could cause issues when applying the patch represented by
|
||||
/// [localArtifact].
|
||||
|
||||
@@ -182,10 +182,12 @@ mixin ShorebirdBuildMixin on ShorebirdCommand {
|
||||
});
|
||||
}
|
||||
|
||||
/// Calls `flutter build ipa`. If [codesign] is false, this will only build
|
||||
/// an .xcarchive and _not_ an .ipa.
|
||||
Future<void> buildIpa({
|
||||
required bool codesign,
|
||||
String? flavor,
|
||||
String? target,
|
||||
bool codesign = true,
|
||||
}) async {
|
||||
return _runShorebirdBuildCommand(() async {
|
||||
const executable = 'flutter';
|
||||
|
||||
@@ -20,6 +20,7 @@ dependencies:
|
||||
googleapis_auth: ^1.4.0
|
||||
http: ^1.0.0
|
||||
intl: ^0.18.0
|
||||
io: ^1.0.4
|
||||
json_annotation: ^4.8.0
|
||||
mason_logger: ^0.2.8
|
||||
meta: ^1.9.0
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
The ipa files in this folder were generated by building the stock Flutter counter app with `shorebird build ipa`.
|
||||
|
||||
Some of their contents has been removed to reduce the size of the files. All .dylib files have been removed.
|
||||
|
||||
Files:
|
||||
|
||||
- base.ipa is meant to represent an ipa uploaded as part of a release. App.framework/App and Flutter.framework/Flutter have been removed to save space.
|
||||
- no_version.ipa an .ipa with the version info removed from the Info.plist, along with several other files to save space.
|
||||
- no_plist.ipa an .ipa with the Info.plist (and many other parts) removed.
|
||||
- app_file_space.ipa is an .ipa with a space in the .app file name.
|
||||
- asset_changes.ipa is built from the same codebase as base.ipa with a change made to one of the assets. App.framework/App and Flutter.framework/Flutter have been removed to save space.
|
||||
- dart_changes.ipa is built from the same codebase as base.ipa with a change made to the dart code. App.framework/App and Flutter.framework/Flutter have been removed to save space.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
The xcarchives in this folder were generated by building the stock Flutter counter app with `shorebird release ios-alpha --no-codesign`. .dylib files have been removed to reduce size.
|
||||
|
||||
Files:
|
||||
|
||||
- Runner.base.xcarchive is meant to represent an xcarchive uploaded as part of a release.
|
||||
- Runner.changed_asset.xcarchive was made with same code as Runner.base.xcarchive, delta a change to a json asset file.
|
||||
- Runner.changed_dart.xcarchive was made with same code as Runner.base.xcarchive, delta a change to lib/main.dart.
|
||||
- Runner.changed_swift.xcarchive was made with same code as Runner.base.xcarchive, delta a change to ios/Runner/AppDelegate.swift.
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -4,11 +4,23 @@ import 'package:shorebird_cli/src/platform.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
final ipaFixturesBasePath = p.join('test', 'fixtures', 'ipas');
|
||||
final baseIpaPath = p.join(ipaFixturesBasePath, 'base.ipa');
|
||||
final changedAssetIpaPath = p.join(ipaFixturesBasePath, 'asset_changes.ipa');
|
||||
final changedDartIpaPath = p.join(ipaFixturesBasePath, 'dart_changes.ipa');
|
||||
final changedSwiftIpaPath = p.join(ipaFixturesBasePath, 'swift_changes.ipa');
|
||||
final xcarchiveFixturesBasePath = p.join('test', 'fixtures', 'xcarchives');
|
||||
final baseIpaPath = p.join(
|
||||
xcarchiveFixturesBasePath,
|
||||
'base.xcarchive.zip',
|
||||
);
|
||||
final changedAssetIpaPath = p.join(
|
||||
xcarchiveFixturesBasePath,
|
||||
'changed_asset.xcarchive.zip',
|
||||
);
|
||||
final changedDartIpaPath = p.join(
|
||||
xcarchiveFixturesBasePath,
|
||||
'changed_dart.xcarchive.zip',
|
||||
);
|
||||
final changedSwiftIpaPath = p.join(
|
||||
xcarchiveFixturesBasePath,
|
||||
'changed_swift.xcarchive.zip',
|
||||
);
|
||||
|
||||
final xcframeworkFixturesBasePath = p.join(
|
||||
'test',
|
||||
@@ -34,29 +46,33 @@ void main() {
|
||||
group('appRegex', () {
|
||||
test('identifies Runner.app/Runner as an app file', () {
|
||||
expect(
|
||||
IosArchiveDiffer.appRegex.hasMatch('Payload/Runner.app/Runner'),
|
||||
IosArchiveDiffer.appRegex.hasMatch(
|
||||
'Products/Applications/Runner.app/Runner',
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('does not identify Runner.app/Assets.car as an app file', () {
|
||||
expect(
|
||||
IosArchiveDiffer.appRegex.hasMatch('Payload/Runner.app/Assets.car'),
|
||||
IosArchiveDiffer.appRegex.hasMatch(
|
||||
'Products/Applications/Runner.app/Assets.car',
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('ipa', () {
|
||||
group('xcarchive', () {
|
||||
group('changedPaths', () {
|
||||
test('finds no differences between the same ipa', () {
|
||||
test('finds no differences between the same xcarchive', () {
|
||||
expect(
|
||||
differ.changedFiles(baseIpaPath, baseIpaPath),
|
||||
isEmpty,
|
||||
);
|
||||
});
|
||||
|
||||
test('finds differences between two different ipas', () {
|
||||
test('finds differences between two different xcarchives', () {
|
||||
final fileSetDiff = differ.changedFiles(
|
||||
baseIpaPath,
|
||||
changedAssetIpaPath,
|
||||
@@ -65,27 +81,19 @@ void main() {
|
||||
expect(
|
||||
fileSetDiff.changedPaths,
|
||||
{
|
||||
'Payload/Runner.app/_CodeSignature/CodeResources',
|
||||
'Payload/Runner.app/Frameworks/App.framework/_CodeSignature/CodeResources',
|
||||
'Payload/Runner.app/Frameworks/App.framework/flutter_assets/assets/asset.json',
|
||||
'Symbols/4C4C4411-5555-3144-A13A-E47369D8ACD5.symbols',
|
||||
'Symbols/BC970605-0A53-3457-8736-D7A870AB6E71.symbols',
|
||||
'Symbols/0CBBC9EF-0745-3074-81B7-765F5B4515FD.symbols',
|
||||
'Products/Applications/Runner.app/Frameworks/App.framework/_CodeSignature/CodeResources',
|
||||
'Products/Applications/Runner.app/Frameworks/App.framework/flutter_assets/assets/asset.json',
|
||||
'Info.plist',
|
||||
},
|
||||
);
|
||||
} else {
|
||||
expect(
|
||||
fileSetDiff.changedPaths,
|
||||
{
|
||||
'Payload/Runner.app/_CodeSignature/CodeResources',
|
||||
'Payload/Runner.app/Runner',
|
||||
'Payload/Runner.app/Frameworks/Flutter.framework/Flutter',
|
||||
'Payload/Runner.app/Frameworks/App.framework/_CodeSignature/CodeResources',
|
||||
'Payload/Runner.app/Frameworks/App.framework/App',
|
||||
'Payload/Runner.app/Frameworks/App.framework/flutter_assets/assets/asset.json',
|
||||
'Symbols/4C4C4411-5555-3144-A13A-E47369D8ACD5.symbols',
|
||||
'Symbols/BC970605-0A53-3457-8736-D7A870AB6E71.symbols',
|
||||
'Symbols/0CBBC9EF-0745-3074-81B7-765F5B4515FD.symbols',
|
||||
'Products/Applications/Runner.app/Frameworks/App.framework/_CodeSignature/CodeResources',
|
||||
'Products/Applications/Runner.app/Frameworks/App.framework/App',
|
||||
'Products/Applications/Runner.app/Frameworks/App.framework/flutter_assets/assets/asset.json',
|
||||
'Info.plist',
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -101,10 +109,7 @@ void main() {
|
||||
differ.dartFileSetDiff(fileSetDiff),
|
||||
platform.isMacOS ? isEmpty : isNotEmpty,
|
||||
);
|
||||
expect(
|
||||
differ.nativeFileSetDiff(fileSetDiff),
|
||||
platform.isMacOS ? isEmpty : isNotEmpty,
|
||||
);
|
||||
expect(differ.nativeFileSetDiff(fileSetDiff), isEmpty);
|
||||
});
|
||||
|
||||
test('detects dart changes', () {
|
||||
@@ -112,20 +117,14 @@ void main() {
|
||||
differ.changedFiles(baseIpaPath, changedDartIpaPath);
|
||||
expect(differ.assetsFileSetDiff(fileSetDiff), isEmpty);
|
||||
expect(differ.dartFileSetDiff(fileSetDiff), isNotEmpty);
|
||||
expect(
|
||||
differ.nativeFileSetDiff(fileSetDiff),
|
||||
platform.isMacOS ? isEmpty : isNotEmpty,
|
||||
);
|
||||
expect(differ.nativeFileSetDiff(fileSetDiff), isEmpty);
|
||||
});
|
||||
|
||||
test('detects swift changes', () {
|
||||
final fileSetDiff =
|
||||
differ.changedFiles(baseIpaPath, changedSwiftIpaPath);
|
||||
expect(differ.assetsFileSetDiff(fileSetDiff), isEmpty);
|
||||
expect(
|
||||
differ.dartFileSetDiff(fileSetDiff),
|
||||
platform.isMacOS ? isEmpty : isNotEmpty,
|
||||
);
|
||||
expect(differ.dartFileSetDiff(fileSetDiff), isEmpty);
|
||||
expect(differ.nativeFileSetDiff(fileSetDiff), isNotEmpty);
|
||||
});
|
||||
});
|
||||
@@ -173,7 +172,7 @@ void main() {
|
||||
);
|
||||
expect(
|
||||
differ.containsPotentiallyBreakingNativeDiffs(fileSetDiff),
|
||||
platform.isMacOS ? isFalse : isTrue,
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
final ipaFixturesBasePath = p.join('test', 'fixtures', 'ipas');
|
||||
final baseIpaBath = p.join(ipaFixturesBasePath, 'base.ipa');
|
||||
final noVersionIpaPath = p.join(ipaFixturesBasePath, 'no_version.ipa');
|
||||
final noPlistIpaPath = p.join(ipaFixturesBasePath, 'no_plist.ipa');
|
||||
final spaceInAppFileNameIpaPath =
|
||||
p.join(ipaFixturesBasePath, 'app_file_space.ipa');
|
||||
|
||||
group(IpaReader, () {
|
||||
test('creates Ipa', () {
|
||||
final ipa = IpaReader().read(baseIpaBath);
|
||||
expect(ipa.versionNumber, '1.0.0+1');
|
||||
});
|
||||
});
|
||||
|
||||
group(Ipa, () {
|
||||
test('reads app version from ipa', () {
|
||||
final ipa = Ipa(path: baseIpaBath);
|
||||
expect(ipa.versionNumber, '1.0.0+1');
|
||||
});
|
||||
|
||||
test('reads app version from ipa with space in app file name', () {
|
||||
final ipa = Ipa(path: spaceInAppFileNameIpaPath);
|
||||
expect(ipa.versionNumber, '1.0.0+1');
|
||||
});
|
||||
|
||||
test('throws exception if no Info.plist is found', () {
|
||||
final ipa = Ipa(path: noPlistIpaPath);
|
||||
expect(() => ipa.versionNumber, throwsException);
|
||||
});
|
||||
|
||||
test('throws exception if no version is found in Info.plist', () {
|
||||
final ipa = Ipa(path: noVersionIpaPath);
|
||||
expect(() => ipa.versionNumber, throwsException);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1483,12 +1483,14 @@ Please bump your version number and try again.''',
|
||||
});
|
||||
|
||||
group('createIosReleaseArtifacts', () {
|
||||
final ipaPath = p.join('path', 'to', 'app.ipa');
|
||||
final xcarchivePath = p.join('path', 'to', 'app.xcarchive');
|
||||
final runnerPath = p.join('path', 'to', 'runner.app');
|
||||
|
||||
Directory setUpTempDir({String? flavor}) {
|
||||
final tempDir = Directory.systemTemp.createTempSync();
|
||||
File(p.join(tempDir.path, ipaPath)).createSync(recursive: true);
|
||||
Directory(p.join(tempDir.path, xcarchivePath))
|
||||
.createSync(recursive: true);
|
||||
Directory(p.join(tempDir.path, runnerPath)).createSync(recursive: true);
|
||||
return tempDir;
|
||||
}
|
||||
|
||||
@@ -1505,12 +1507,14 @@ Please bump your version number and try again.''',
|
||||
).thenAnswer((_) async => {});
|
||||
});
|
||||
|
||||
test('exits with code 70 when ipa artifact creation fails', () async {
|
||||
test('exits with code 70 when xcarchive artifact creation fails',
|
||||
() async {
|
||||
const error = 'something went wrong';
|
||||
when(
|
||||
() => codePushClient.createReleaseArtifact(
|
||||
appId: any(named: 'appId'),
|
||||
artifactPath: any(named: 'artifactPath', that: endsWith('.ipa')),
|
||||
artifactPath:
|
||||
any(named: 'artifactPath', that: endsWith('.xcarchive.zip')),
|
||||
releaseId: any(named: 'releaseId'),
|
||||
arch: any(named: 'arch'),
|
||||
platform: any(named: 'platform'),
|
||||
@@ -1525,7 +1529,7 @@ Please bump your version number and try again.''',
|
||||
() async => codePushClientWrapper.createIosReleaseArtifacts(
|
||||
appId: app.appId,
|
||||
releaseId: releaseId,
|
||||
ipaPath: p.join(tempDir.path, ipaPath),
|
||||
xcarchivePath: p.join(tempDir.path, xcarchivePath),
|
||||
runnerPath: p.join(tempDir.path, runnerPath),
|
||||
),
|
||||
),
|
||||
@@ -1537,13 +1541,14 @@ Please bump your version number and try again.''',
|
||||
verify(() => progress.fail(any(that: contains(error)))).called(1);
|
||||
});
|
||||
|
||||
test('exits with code 70 when uploading ipa that already exists',
|
||||
test('exits with code 70 when uploading xcarchive that already exists',
|
||||
() async {
|
||||
const error = 'something went wrong';
|
||||
when(
|
||||
() => codePushClient.createReleaseArtifact(
|
||||
appId: any(named: 'appId'),
|
||||
artifactPath: any(named: 'artifactPath', that: endsWith('.ipa')),
|
||||
artifactPath:
|
||||
any(named: 'artifactPath', that: endsWith('.xcarchive.zip')),
|
||||
releaseId: any(named: 'releaseId'),
|
||||
arch: any(named: 'arch'),
|
||||
platform: any(named: 'platform'),
|
||||
@@ -1558,7 +1563,7 @@ Please bump your version number and try again.''',
|
||||
() async => codePushClientWrapper.createIosReleaseArtifacts(
|
||||
appId: app.appId,
|
||||
releaseId: releaseId,
|
||||
ipaPath: p.join(tempDir.path, ipaPath),
|
||||
xcarchivePath: p.join(tempDir.path, xcarchivePath),
|
||||
runnerPath: p.join(tempDir.path, runnerPath),
|
||||
),
|
||||
),
|
||||
@@ -1594,7 +1599,7 @@ Please bump your version number and try again.''',
|
||||
() async => codePushClientWrapper.createIosReleaseArtifacts(
|
||||
appId: app.appId,
|
||||
releaseId: releaseId,
|
||||
ipaPath: p.join(tempDir.path, ipaPath),
|
||||
xcarchivePath: p.join(tempDir.path, xcarchivePath),
|
||||
runnerPath: p.join(tempDir.path, runnerPath),
|
||||
),
|
||||
),
|
||||
@@ -1624,7 +1629,7 @@ Please bump your version number and try again.''',
|
||||
() async => codePushClientWrapper.createIosReleaseArtifacts(
|
||||
appId: app.appId,
|
||||
releaseId: releaseId,
|
||||
ipaPath: p.join(tempDir.path, ipaPath),
|
||||
xcarchivePath: p.join(tempDir.path, xcarchivePath),
|
||||
runnerPath: p.join(tempDir.path, runnerPath),
|
||||
),
|
||||
getCurrentDirectory: () => tempDir,
|
||||
|
||||
@@ -189,7 +189,8 @@ Please make sure you are running "shorebird init" from the root of your Flutter
|
||||
final exitCode = await runWithOverrides(command.run);
|
||||
verify(
|
||||
() => logger.err(
|
||||
'A "shorebird.yaml" file already exists and seems up-to-date.'),
|
||||
'A "shorebird.yaml" file already exists and seems up-to-date.',
|
||||
),
|
||||
).called(1);
|
||||
verify(
|
||||
() => logger.info(
|
||||
|
||||
@@ -37,12 +37,8 @@ class _MockCodePushClientWrapper extends Mock
|
||||
|
||||
class _MockDoctor extends Mock implements Doctor {}
|
||||
|
||||
class _MockIpa extends Mock implements Ipa {}
|
||||
|
||||
class _MockIpaDiffer extends Mock implements IosArchiveDiffer {}
|
||||
|
||||
class _MockIpaReader extends Mock implements IpaReader {}
|
||||
|
||||
class _MockLogger extends Mock implements Logger {}
|
||||
|
||||
class _MockPatchDiffChecker extends Mock implements PatchDiffChecker {}
|
||||
@@ -85,10 +81,40 @@ void main() {
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleName</key>
|
||||
<string>app_bundle_name</string>
|
||||
<key>ApplicationProperties</key>
|
||||
<dict>
|
||||
<key>ApplicationPath</key>
|
||||
<string>Applications/Runner.app</string>
|
||||
<key>Architectures</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.shorebird.timeShift</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.2.3</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
<key>ArchiveVersion</key>
|
||||
<integer>2</integer>
|
||||
<key>Name</key>
|
||||
<string>Runner</string>
|
||||
<key>SchemeName</key>
|
||||
<string>Runner</string>
|
||||
</dict>
|
||||
</plist>''';
|
||||
const emptyPlistContent = '''
|
||||
<?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>ApplicationProperties</key>
|
||||
<dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>'
|
||||
''';
|
||||
const pubspecYamlContent = '''
|
||||
name: example
|
||||
version: $version
|
||||
@@ -126,9 +152,7 @@ flutter:
|
||||
late Directory shorebirdRoot;
|
||||
late File genSnapshotFile;
|
||||
late Doctor doctor;
|
||||
late Ipa ipa;
|
||||
late IosArchiveDiffer archiveDiffer;
|
||||
late IpaReader ipaReader;
|
||||
late Progress progress;
|
||||
late Logger logger;
|
||||
late PatchDiffChecker patchDiffChecker;
|
||||
@@ -171,7 +195,14 @@ flutter:
|
||||
p.join(tempDir.path, 'shorebird.yaml'),
|
||||
).writeAsStringSync('app_id: $appId');
|
||||
File(
|
||||
p.join(tempDir.path, 'ios', 'Runner', 'Info.plist'),
|
||||
p.join(
|
||||
tempDir.path,
|
||||
'build',
|
||||
'ios',
|
||||
'archive',
|
||||
'Runner.xcarchive',
|
||||
'Info.plist',
|
||||
),
|
||||
)
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync(infoPlistContent);
|
||||
@@ -199,6 +230,7 @@ flutter:
|
||||
}
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(Directory(''));
|
||||
registerFallbackValue(File(''));
|
||||
registerFallbackValue(FileSetDiff.empty());
|
||||
registerFallbackValue(ReleasePlatform.ios);
|
||||
@@ -227,9 +259,7 @@ flutter:
|
||||
'gen_snapshot_arm64',
|
||||
),
|
||||
);
|
||||
ipa = _MockIpa();
|
||||
archiveDiffer = _MockIpaDiffer();
|
||||
ipaReader = _MockIpaReader();
|
||||
progress = _MockProgress();
|
||||
logger = _MockLogger();
|
||||
platform = _MockPlatform();
|
||||
@@ -277,8 +307,6 @@ flutter:
|
||||
patchArtifactBundles: any(named: 'patchArtifactBundles'),
|
||||
),
|
||||
).thenAnswer((_) async {});
|
||||
when(() => ipa.versionNumber).thenReturn(version);
|
||||
when(() => ipaReader.read(any())).thenReturn(ipa);
|
||||
when(() => doctor.iosCommandValidators).thenReturn([flutterValidator]);
|
||||
when(flutterValidator.validate).thenAnswer((_) async => []);
|
||||
when(() => logger.confirm(any())).thenReturn(true);
|
||||
@@ -333,8 +361,8 @@ flutter:
|
||||
),
|
||||
).thenAnswer((_) async {});
|
||||
when(
|
||||
() => patchDiffChecker.confirmUnpatchableDiffsIfNecessary(
|
||||
localArtifact: any(named: 'localArtifact'),
|
||||
() => patchDiffChecker.zipAndConfirmUnpatchableDiffsIfNecessary(
|
||||
localArtifactDirectory: any(named: 'localArtifactDirectory'),
|
||||
releaseArtifactUrl: any(named: 'releaseArtifactUrl'),
|
||||
archiveDiffer: archiveDiffer,
|
||||
force: any(named: 'force'),
|
||||
@@ -342,8 +370,7 @@ flutter:
|
||||
).thenAnswer((_) async => {});
|
||||
|
||||
command = runWithOverrides(
|
||||
() =>
|
||||
PatchIosCommand(archiveDiffer: archiveDiffer, ipaReader: ipaReader),
|
||||
() => PatchIosCommand(archiveDiffer: archiveDiffer),
|
||||
)..testArgResults = argResults;
|
||||
});
|
||||
|
||||
@@ -451,63 +478,7 @@ error: exportArchive: No signing certificate "iOS Distribution" found
|
||||
|
||||
expect(exitCode, equals(ExitCode.software.code));
|
||||
verify(
|
||||
() => progress.fail(
|
||||
any(
|
||||
that: stringContainsInOrder([
|
||||
'Could not find ipa file',
|
||||
'No directory found at ${p.join(tempDir.path, 'build')}',
|
||||
]),
|
||||
),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('exits with code 70 if ipa file does not exist', () async {
|
||||
final tempDir = setUpTempDir();
|
||||
setUpTempArtifacts(tempDir);
|
||||
File(p.join(tempDir.path, ipaPath)).deleteSync(recursive: true);
|
||||
|
||||
final exitCode = await IOOverrides.runZoned(
|
||||
() => runWithOverrides(command.run),
|
||||
getCurrentDirectory: () => tempDir,
|
||||
);
|
||||
|
||||
expect(exitCode, equals(ExitCode.software.code));
|
||||
verify(
|
||||
() => progress.fail(
|
||||
any(
|
||||
that: stringContainsInOrder([
|
||||
'Could not find ipa file',
|
||||
'No .ipa files found in',
|
||||
'build/ios/ipa',
|
||||
]),
|
||||
),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('exits with code 70 if more than one ipa file is found', () async {
|
||||
final tempDir = setUpTempDir();
|
||||
setUpTempArtifacts(tempDir);
|
||||
File(p.join(tempDir.path, 'build/ios/ipa/Runner2.ipa'))
|
||||
.createSync(recursive: true);
|
||||
|
||||
final exitCode = await IOOverrides.runZoned(
|
||||
() => runWithOverrides(command.run),
|
||||
getCurrentDirectory: () => tempDir,
|
||||
);
|
||||
|
||||
expect(exitCode, equals(ExitCode.software.code));
|
||||
verify(
|
||||
() => progress.fail(
|
||||
any(
|
||||
that: stringContainsInOrder([
|
||||
'Could not find ipa file',
|
||||
'More than one .ipa file found in',
|
||||
'build/ios/ipa',
|
||||
]),
|
||||
),
|
||||
),
|
||||
() => logger.err(any(that: contains('No Info.plist file found'))),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
@@ -659,10 +630,21 @@ Please re-run the release command for this version or create a new release.'''),
|
||||
|
||||
test('exits with code 70 when release version cannot be determiend',
|
||||
() async {
|
||||
when(() => ipa.versionNumber).thenThrow(Exception('oops'));
|
||||
|
||||
final tempDir = setUpTempDir();
|
||||
setUpTempArtifacts(tempDir);
|
||||
File(
|
||||
p.join(
|
||||
tempDir.path,
|
||||
'build',
|
||||
'ios',
|
||||
'archive',
|
||||
'Runner.xcarchive',
|
||||
'Info.plist',
|
||||
),
|
||||
)
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync(emptyPlistContent);
|
||||
|
||||
final exitCode = await IOOverrides.runZoned(
|
||||
() => runWithOverrides(command.run),
|
||||
getCurrentDirectory: () => tempDir,
|
||||
@@ -670,7 +652,7 @@ Please re-run the release command for this version or create a new release.'''),
|
||||
|
||||
expect(exitCode, equals(ExitCode.software.code));
|
||||
verify(
|
||||
() => progress.fail(
|
||||
() => logger.err(
|
||||
any(that: contains('Failed to determine release version')),
|
||||
),
|
||||
).called(1);
|
||||
@@ -679,14 +661,14 @@ Please re-run the release command for this version or create a new release.'''),
|
||||
test('prints release version when detected', () async {
|
||||
final tempDir = setUpTempDir();
|
||||
setUpTempArtifacts(tempDir);
|
||||
|
||||
final exitCode = await IOOverrides.runZoned(
|
||||
() => runWithOverrides(command.run),
|
||||
getCurrentDirectory: () => tempDir,
|
||||
);
|
||||
|
||||
expect(exitCode, equals(ExitCode.success.code));
|
||||
verify(() => progress.complete('Detected release version 1.2.3+1'))
|
||||
.called(1);
|
||||
verify(() => logger.info('Detected release version 1.2.3+1')).called(1);
|
||||
});
|
||||
|
||||
test('aborts when user opts out', () async {
|
||||
@@ -717,62 +699,13 @@ Please re-run the release command for this version or create a new release.'''),
|
||||
expect(exitCode, ExitCode.software.code);
|
||||
});
|
||||
|
||||
test('exits with code 70 when ipa not found', () async {
|
||||
final tempDir = setUpTempDir();
|
||||
setUpTempArtifacts(tempDir);
|
||||
File(p.join(tempDir.path, ipaPath)).deleteSync();
|
||||
|
||||
final exitCode = await IOOverrides.runZoned(
|
||||
() => runWithOverrides(command.run),
|
||||
getCurrentDirectory: () => tempDir,
|
||||
);
|
||||
|
||||
expect(exitCode, equals(ExitCode.software.code));
|
||||
verify(
|
||||
() => progress.fail(any(that: contains('Could not find ipa file'))),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('exits with code 70 when local release version cannot be determiend',
|
||||
() async {
|
||||
when(() => ipa.versionNumber).thenThrow(Exception('oops'));
|
||||
|
||||
final tempDir = setUpTempDir();
|
||||
setUpTempArtifacts(tempDir);
|
||||
final exitCode = await IOOverrides.runZoned(
|
||||
() => runWithOverrides(command.run),
|
||||
getCurrentDirectory: () => tempDir,
|
||||
);
|
||||
|
||||
expect(exitCode, equals(ExitCode.software.code));
|
||||
verify(
|
||||
() => progress.fail(
|
||||
any(that: contains('Failed to determine release version')),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('prints local release version when detected', () async {
|
||||
final tempDir = setUpTempDir();
|
||||
setUpTempArtifacts(tempDir);
|
||||
final exitCode = await IOOverrides.runZoned(
|
||||
() => runWithOverrides(command.run),
|
||||
getCurrentDirectory: () => tempDir,
|
||||
);
|
||||
|
||||
expect(exitCode, equals(ExitCode.success.code));
|
||||
verify(
|
||||
() => progress.complete('Detected release version 1.2.3+1'),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test(
|
||||
'''exits with code 0 if confirmUnpatchableDiffsIfNecessary throws UserCancelledException''',
|
||||
'''exits with code 0 if zipAndConfirmUnpatchableDiffsIfNecessary throws UserCancelledException''',
|
||||
() async {
|
||||
when(() => argResults['force']).thenReturn(false);
|
||||
when(
|
||||
() => patchDiffChecker.confirmUnpatchableDiffsIfNecessary(
|
||||
localArtifact: any(named: 'localArtifact'),
|
||||
() => patchDiffChecker.zipAndConfirmUnpatchableDiffsIfNecessary(
|
||||
localArtifactDirectory: any(named: 'localArtifactDirectory'),
|
||||
releaseArtifactUrl: any(named: 'releaseArtifactUrl'),
|
||||
archiveDiffer: archiveDiffer,
|
||||
force: any(named: 'force'),
|
||||
@@ -788,8 +721,8 @@ Please re-run the release command for this version or create a new release.'''),
|
||||
|
||||
expect(exitCode, equals(ExitCode.success.code));
|
||||
verify(
|
||||
() => patchDiffChecker.confirmUnpatchableDiffsIfNecessary(
|
||||
localArtifact: any(named: 'localArtifact'),
|
||||
() => patchDiffChecker.zipAndConfirmUnpatchableDiffsIfNecessary(
|
||||
localArtifactDirectory: any(named: 'localArtifactDirectory'),
|
||||
releaseArtifactUrl: Uri.parse(ipaArtifact.url),
|
||||
archiveDiffer: archiveDiffer,
|
||||
force: false,
|
||||
@@ -807,12 +740,12 @@ Please re-run the release command for this version or create a new release.'''),
|
||||
});
|
||||
|
||||
test(
|
||||
'''exits with code 70 if confirmUnpatchableDiffsIfNecessary throws UnpatchableChangeException''',
|
||||
'''exits with code 70 if zipAndConfirmUnpatchableDiffsIfNecessary throws UnpatchableChangeException''',
|
||||
() async {
|
||||
when(() => argResults['force']).thenReturn(false);
|
||||
when(
|
||||
() => patchDiffChecker.confirmUnpatchableDiffsIfNecessary(
|
||||
localArtifact: any(named: 'localArtifact'),
|
||||
() => patchDiffChecker.zipAndConfirmUnpatchableDiffsIfNecessary(
|
||||
localArtifactDirectory: any(named: 'localArtifactDirectory'),
|
||||
releaseArtifactUrl: any(named: 'releaseArtifactUrl'),
|
||||
archiveDiffer: archiveDiffer,
|
||||
force: any(named: 'force'),
|
||||
@@ -828,8 +761,8 @@ Please re-run the release command for this version or create a new release.'''),
|
||||
|
||||
expect(exitCode, equals(ExitCode.software.code));
|
||||
verify(
|
||||
() => patchDiffChecker.confirmUnpatchableDiffsIfNecessary(
|
||||
localArtifact: any(named: 'localArtifact'),
|
||||
() => patchDiffChecker.zipAndConfirmUnpatchableDiffsIfNecessary(
|
||||
localArtifactDirectory: any(named: 'localArtifactDirectory'),
|
||||
releaseArtifactUrl: Uri.parse(ipaArtifact.url),
|
||||
archiveDiffer: archiveDiffer,
|
||||
force: false,
|
||||
@@ -933,6 +866,33 @@ Please re-run the release command for this version or create a new release.'''),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('forwards codesign to flutter build', () async {
|
||||
when(() => argResults['codesign']).thenReturn(false);
|
||||
final tempDir = setUpTempDir();
|
||||
setUpTempArtifacts(tempDir);
|
||||
await IOOverrides.runZoned(
|
||||
() => runWithOverrides(command.run),
|
||||
getCurrentDirectory: () => tempDir,
|
||||
);
|
||||
|
||||
verify(
|
||||
() => shorebirdProcess.run(
|
||||
'flutter',
|
||||
any(
|
||||
that: containsAllInOrder(
|
||||
[
|
||||
'build',
|
||||
'ipa',
|
||||
'--release',
|
||||
'--no-codesign',
|
||||
],
|
||||
),
|
||||
),
|
||||
runInShell: true,
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test(
|
||||
'succeeds when patch is successful '
|
||||
'with flavors and target', () async {
|
||||
|
||||
+13
-12
@@ -179,6 +179,7 @@ flutter:
|
||||
}
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(Directory(''));
|
||||
registerFallbackValue(File(''));
|
||||
registerFallbackValue(ReleasePlatform.ios);
|
||||
registerFallbackValue(Uri.parse('https://example.com'));
|
||||
@@ -300,8 +301,8 @@ flutter:
|
||||
),
|
||||
).thenAnswer((_) async {});
|
||||
when(
|
||||
() => patchDiffChecker.confirmUnpatchableDiffsIfNecessary(
|
||||
localArtifact: any(named: 'localArtifact'),
|
||||
() => patchDiffChecker.zipAndConfirmUnpatchableDiffsIfNecessary(
|
||||
localArtifactDirectory: any(named: 'localArtifactDirectory'),
|
||||
releaseArtifactUrl: any(named: 'releaseArtifactUrl'),
|
||||
archiveDiffer: archiveDiffer,
|
||||
force: any(named: 'force'),
|
||||
@@ -633,12 +634,12 @@ Please re-run the release command for this version or create a new release.'''),
|
||||
});
|
||||
|
||||
test(
|
||||
'''exits with code 0 if confirmUnpatchableDiffsIfNecessary throws UserCancelledException''',
|
||||
'''exits with code 0 if zipAndConfirmUnpatchableDiffsIfNecessary throws UserCancelledException''',
|
||||
() async {
|
||||
when(() => argResults['force']).thenReturn(false);
|
||||
when(
|
||||
() => patchDiffChecker.confirmUnpatchableDiffsIfNecessary(
|
||||
localArtifact: any(named: 'localArtifact'),
|
||||
() => patchDiffChecker.zipAndConfirmUnpatchableDiffsIfNecessary(
|
||||
localArtifactDirectory: any(named: 'localArtifactDirectory'),
|
||||
releaseArtifactUrl: any(named: 'releaseArtifactUrl'),
|
||||
archiveDiffer: archiveDiffer,
|
||||
force: any(named: 'force'),
|
||||
@@ -654,8 +655,8 @@ Please re-run the release command for this version or create a new release.'''),
|
||||
|
||||
expect(exitCode, equals(ExitCode.success.code));
|
||||
verify(
|
||||
() => patchDiffChecker.confirmUnpatchableDiffsIfNecessary(
|
||||
localArtifact: any(named: 'localArtifact'),
|
||||
() => patchDiffChecker.zipAndConfirmUnpatchableDiffsIfNecessary(
|
||||
localArtifactDirectory: any(named: 'localArtifactDirectory'),
|
||||
releaseArtifactUrl: Uri.parse(xcframeworkArtifact.url),
|
||||
archiveDiffer: archiveDiffer,
|
||||
force: false,
|
||||
@@ -673,12 +674,12 @@ Please re-run the release command for this version or create a new release.'''),
|
||||
});
|
||||
|
||||
test(
|
||||
'''exits with code 70 if confirmUnpatchableDiffsIfNecessary throws UnpatchableChangeException''',
|
||||
'''exits with code 70 if zipAndConfirmUnpatchableDiffsIfNecessary throws UnpatchableChangeException''',
|
||||
() async {
|
||||
when(() => argResults['force']).thenReturn(false);
|
||||
when(
|
||||
() => patchDiffChecker.confirmUnpatchableDiffsIfNecessary(
|
||||
localArtifact: any(named: 'localArtifact'),
|
||||
() => patchDiffChecker.zipAndConfirmUnpatchableDiffsIfNecessary(
|
||||
localArtifactDirectory: any(named: 'localArtifactDirectory'),
|
||||
releaseArtifactUrl: any(named: 'releaseArtifactUrl'),
|
||||
archiveDiffer: archiveDiffer,
|
||||
force: any(named: 'force'),
|
||||
@@ -694,8 +695,8 @@ Please re-run the release command for this version or create a new release.'''),
|
||||
|
||||
expect(exitCode, equals(ExitCode.software.code));
|
||||
verify(
|
||||
() => patchDiffChecker.confirmUnpatchableDiffsIfNecessary(
|
||||
localArtifact: any(named: 'localArtifact'),
|
||||
() => patchDiffChecker.zipAndConfirmUnpatchableDiffsIfNecessary(
|
||||
localArtifactDirectory: any(named: 'localArtifactDirectory'),
|
||||
releaseArtifactUrl: Uri.parse(xcframeworkArtifact.url),
|
||||
archiveDiffer: archiveDiffer,
|
||||
force: false,
|
||||
|
||||
@@ -357,12 +357,20 @@ void main() {
|
||||
test('succeeds when release is successful', () async {
|
||||
final exitCode = await runWithOverrides(command.run);
|
||||
verify(() => logger.success('\n✅ Published Release!')).called(1);
|
||||
final aabPath = p.join(
|
||||
'build',
|
||||
'app',
|
||||
'outputs',
|
||||
'bundle',
|
||||
'release',
|
||||
'app-release.aab',
|
||||
);
|
||||
// Verify info message does not include apk instructions.
|
||||
verify(
|
||||
() => logger.info('''
|
||||
|
||||
Your next step is to upload the app bundle to the Play Store:
|
||||
${lightCyan.wrap('build/app/outputs/bundle/release/app-release.aab')}
|
||||
${lightCyan.wrap(aabPath)}
|
||||
|
||||
For information on uploading to the Play Store, see:
|
||||
${link(uri: Uri.parse('https://support.google.com/googleplay/android-developer/answer/9859152?hl=en'))}
|
||||
@@ -393,14 +401,30 @@ ${link(uri: Uri.parse('https://support.google.com/googleplay/android-developer/a
|
||||
final exitCode = await runWithOverrides(command.run);
|
||||
verify(() => logger.success('\n✅ Published Release!')).called(1);
|
||||
// Verify info message does include apk instructions.
|
||||
final aabPath = p.join(
|
||||
'build',
|
||||
'app',
|
||||
'outputs',
|
||||
'bundle',
|
||||
'release',
|
||||
'app-release.aab',
|
||||
);
|
||||
final apkPath = p.join(
|
||||
'build',
|
||||
'app',
|
||||
'outputs',
|
||||
'apk',
|
||||
'release',
|
||||
'app-release.apk',
|
||||
);
|
||||
verify(
|
||||
() => logger.info('''
|
||||
|
||||
Your next step is to upload the app bundle to the Play Store:
|
||||
${lightCyan.wrap('build/app/outputs/bundle/release/app-release.aab')}
|
||||
${lightCyan.wrap(aabPath)}
|
||||
|
||||
Or distribute the apk:
|
||||
${lightCyan.wrap('build/app/outputs/apk/release/app-release.apk')}
|
||||
${lightCyan.wrap(apkPath)}
|
||||
|
||||
For information on uploading to the Play Store, see:
|
||||
${link(uri: Uri.parse('https://support.google.com/googleplay/android-developer/answer/9859152?hl=en'))}
|
||||
|
||||
@@ -7,7 +7,6 @@ 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';
|
||||
@@ -31,10 +30,6 @@ class _MockCodePushClientWrapper extends Mock
|
||||
|
||||
class _MockDoctor extends Mock implements Doctor {}
|
||||
|
||||
class _MockIpa extends Mock implements Ipa {}
|
||||
|
||||
class _MockIpaReader extends Mock implements IpaReader {}
|
||||
|
||||
class _MockLogger extends Mock implements Logger {}
|
||||
|
||||
class _MockPlatform extends Mock implements Platform {}
|
||||
@@ -82,10 +77,40 @@ void main() {
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleName</key>
|
||||
<string>app_bundle_name</string>
|
||||
<key>ApplicationProperties</key>
|
||||
<dict>
|
||||
<key>ApplicationPath</key>
|
||||
<string>Applications/Runner.app</string>
|
||||
<key>Architectures</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.shorebird.timeShift</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.2.3</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
<key>ArchiveVersion</key>
|
||||
<integer>2</integer>
|
||||
<key>Name</key>
|
||||
<string>Runner</string>
|
||||
<key>SchemeName</key>
|
||||
<string>Runner</string>
|
||||
</dict>
|
||||
</plist>''';
|
||||
const emptyPlistContent = '''
|
||||
<?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>ApplicationProperties</key>
|
||||
<dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>'
|
||||
''';
|
||||
const pubspecYamlContent = '''
|
||||
name: example
|
||||
version: $version
|
||||
@@ -102,8 +127,6 @@ flutter:
|
||||
late Doctor doctor;
|
||||
late Platform platform;
|
||||
late Auth auth;
|
||||
late IpaReader ipaReader;
|
||||
late Ipa ipa;
|
||||
late Progress progress;
|
||||
late Logger logger;
|
||||
late ShorebirdProcessResult flutterBuildProcessResult;
|
||||
@@ -139,7 +162,14 @@ flutter:
|
||||
p.join(tempDir.path, 'shorebird.yaml'),
|
||||
).writeAsStringSync('app_id: $appId');
|
||||
File(
|
||||
p.join(tempDir.path, 'ios', 'Runner', 'Info.plist'),
|
||||
p.join(
|
||||
tempDir.path,
|
||||
'build',
|
||||
'ios',
|
||||
'archive',
|
||||
'Runner.xcarchive',
|
||||
'Info.plist',
|
||||
),
|
||||
)
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync(infoPlistContent);
|
||||
@@ -161,8 +191,6 @@ flutter:
|
||||
platform = _MockPlatform();
|
||||
shorebirdRoot = Directory.systemTemp.createTempSync();
|
||||
auth = _MockAuth();
|
||||
ipa = _MockIpa();
|
||||
ipaReader = _MockIpaReader();
|
||||
progress = _MockProgress();
|
||||
logger = _MockLogger();
|
||||
flutterBuildProcessResult = _MockProcessResult();
|
||||
@@ -193,10 +221,9 @@ flutter:
|
||||
).thenAnswer((_) async => flutterBuildProcessResult);
|
||||
when(() => argResults.rest).thenReturn([]);
|
||||
when(() => argResults['arch']).thenReturn(arch);
|
||||
when(() => argResults['codesign']).thenReturn(true);
|
||||
when(() => argResults['platform']).thenReturn(releasePlatform);
|
||||
when(() => auth.isAuthenticated).thenReturn(true);
|
||||
when(() => ipaReader.read(any())).thenReturn(ipa);
|
||||
when(() => ipa.versionNumber).thenReturn(version);
|
||||
when(() => logger.progress(any())).thenReturn(progress);
|
||||
when(() => logger.confirm(any())).thenReturn(true);
|
||||
when(
|
||||
@@ -235,7 +262,7 @@ flutter:
|
||||
() => codePushClientWrapper.createIosReleaseArtifacts(
|
||||
appId: any(named: 'appId'),
|
||||
releaseId: any(named: 'releaseId'),
|
||||
ipaPath: any(named: 'ipaPath'),
|
||||
xcarchivePath: any(named: 'xcarchivePath'),
|
||||
runnerPath: any(named: 'runnerPath'),
|
||||
),
|
||||
).thenAnswer((_) async => release);
|
||||
@@ -258,7 +285,7 @@ flutter:
|
||||
),
|
||||
).thenAnswer((_) async {});
|
||||
|
||||
command = runWithOverrides(() => ReleaseIosCommand(ipaReader: ipaReader))
|
||||
command = runWithOverrides(ReleaseIosCommand.new)
|
||||
..testArgResults = argResults;
|
||||
});
|
||||
|
||||
@@ -290,6 +317,84 @@ flutter:
|
||||
).called(1);
|
||||
});
|
||||
|
||||
group('when codesign is disabled', () {
|
||||
setUp(() {
|
||||
when(() => argResults['codesign']).thenReturn(false);
|
||||
});
|
||||
|
||||
test('prints instructions to manually codesign', () async {
|
||||
final tempDir = setUpTempDir();
|
||||
await IOOverrides.runZoned(
|
||||
() => runWithOverrides(command.run),
|
||||
getCurrentDirectory: () => tempDir,
|
||||
);
|
||||
|
||||
verify(
|
||||
() => logger.info(
|
||||
'''Building for device with codesigning disabled. You will have to manually codesign before deploying to device.''',
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('builds without codesigning', () async {
|
||||
final tempDir = setUpTempDir();
|
||||
await IOOverrides.runZoned(
|
||||
() => runWithOverrides(command.run),
|
||||
getCurrentDirectory: () => tempDir,
|
||||
);
|
||||
|
||||
verify(
|
||||
() => shorebirdProcess.run(
|
||||
'flutter',
|
||||
any(
|
||||
that: containsAllInOrder(
|
||||
[
|
||||
'build',
|
||||
'ipa',
|
||||
'--release',
|
||||
'--no-codesign',
|
||||
],
|
||||
),
|
||||
),
|
||||
runInShell: true,
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('prints archive upload instructions on success', () async {
|
||||
final tempDir = setUpTempDir();
|
||||
final result = await IOOverrides.runZoned(
|
||||
() => runWithOverrides(command.run),
|
||||
getCurrentDirectory: () => tempDir,
|
||||
);
|
||||
|
||||
expect(result, equals(ExitCode.success.code));
|
||||
final archivePath = p.join(
|
||||
tempDir.path,
|
||||
'build',
|
||||
'ios',
|
||||
'archive',
|
||||
'Runner.xcarchive',
|
||||
);
|
||||
verify(
|
||||
() => logger.info(
|
||||
any(
|
||||
that: stringContainsInOrder(
|
||||
[
|
||||
'Your next step is to submit the archive',
|
||||
archivePath,
|
||||
'to the App Store using Xcode.',
|
||||
'You can open the archive in Xcode by running',
|
||||
'open $archivePath',
|
||||
'''Make sure to uncheck "Manage Version and Build Number", or else shorebird will not work.''',
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('exits with code 70 when build fails with non-zero exit code',
|
||||
() async {
|
||||
when(() => flutterBuildProcessResult.exitCode).thenReturn(1);
|
||||
@@ -346,9 +451,19 @@ error: exportArchive: No signing certificate "iOS Distribution" found
|
||||
|
||||
test('exits with code 70 when release version cannot be determiend',
|
||||
() async {
|
||||
when(() => ipa.versionNumber).thenThrow(Exception('oops'));
|
||||
|
||||
final tempDir = setUpTempDir();
|
||||
File(
|
||||
p.join(
|
||||
tempDir.path,
|
||||
'build',
|
||||
'ios',
|
||||
'archive',
|
||||
'Runner.xcarchive',
|
||||
'Info.plist',
|
||||
),
|
||||
)
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync(emptyPlistContent);
|
||||
final exitCode = await IOOverrides.runZoned(
|
||||
() => runWithOverrides(command.run),
|
||||
getCurrentDirectory: () => tempDir,
|
||||
@@ -356,7 +471,7 @@ error: exportArchive: No signing certificate "iOS Distribution" found
|
||||
|
||||
expect(exitCode, equals(ExitCode.software.code));
|
||||
verify(
|
||||
() => progress.fail(
|
||||
() => logger.err(
|
||||
any(that: contains('Failed to determine release version')),
|
||||
),
|
||||
).called(1);
|
||||
@@ -377,12 +492,37 @@ error: exportArchive: No signing certificate "iOS Distribution" found
|
||||
() => codePushClientWrapper.createIosReleaseArtifacts(
|
||||
appId: appId,
|
||||
releaseId: release.id,
|
||||
ipaPath: any(named: 'ipaPath', that: endsWith('.ipa')),
|
||||
xcarchivePath:
|
||||
any(named: 'xcarchivePath', that: endsWith('.xcarchive')),
|
||||
runnerPath: any(named: 'runnerPath', that: endsWith('Runner.app')),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('exits with code 70 if Info.plist does not exist', () async {
|
||||
final tempDir = setUpTempDir();
|
||||
final infoPlistFile = File(
|
||||
p.join(
|
||||
tempDir.path,
|
||||
'build',
|
||||
'ios',
|
||||
'archive',
|
||||
'Runner.xcarchive',
|
||||
'Info.plist',
|
||||
),
|
||||
)..deleteSync(recursive: true);
|
||||
|
||||
final exitCode = await IOOverrides.runZoned(
|
||||
() => runWithOverrides(command.run),
|
||||
getCurrentDirectory: () => tempDir,
|
||||
);
|
||||
|
||||
expect(exitCode, equals(ExitCode.software.code));
|
||||
verify(
|
||||
() => logger.err('No Info.plist file found at ${infoPlistFile.path}.'),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('exits with code 70 if build directory does not exist', () async {
|
||||
final tempDir = setUpTempDir();
|
||||
Directory(p.join(tempDir.path, 'build')).deleteSync(recursive: true);
|
||||
@@ -394,12 +534,31 @@ error: exportArchive: No signing certificate "iOS Distribution" found
|
||||
|
||||
expect(exitCode, equals(ExitCode.software.code));
|
||||
verify(
|
||||
() => progress.fail(
|
||||
() => logger.err(any(that: contains('No directory found'))),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('exits with code 70 if ipa build directory does not exist', () async {
|
||||
final tempDir = setUpTempDir();
|
||||
final ipaDirectory =
|
||||
Directory(p.join(tempDir.path, 'build', 'ios', 'ipa'))
|
||||
..deleteSync(recursive: true);
|
||||
|
||||
final exitCode = await IOOverrides.runZoned(
|
||||
() => runWithOverrides(command.run),
|
||||
getCurrentDirectory: () => tempDir,
|
||||
);
|
||||
|
||||
expect(exitCode, equals(ExitCode.software.code));
|
||||
verify(
|
||||
() => logger.err(
|
||||
any(
|
||||
that: stringContainsInOrder([
|
||||
'Could not find ipa file',
|
||||
'No directory found at ${p.join(tempDir.path, 'build')}',
|
||||
]),
|
||||
that: stringContainsInOrder(
|
||||
[
|
||||
'Could not find ipa file',
|
||||
'No directory found at ${ipaDirectory.path}',
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
).called(1);
|
||||
@@ -416,7 +575,7 @@ error: exportArchive: No signing certificate "iOS Distribution" found
|
||||
|
||||
expect(exitCode, equals(ExitCode.software.code));
|
||||
verify(
|
||||
() => progress.fail(
|
||||
() => logger.err(
|
||||
any(
|
||||
that: stringContainsInOrder([
|
||||
'Could not find ipa file',
|
||||
@@ -440,7 +599,7 @@ error: exportArchive: No signing certificate "iOS Distribution" found
|
||||
|
||||
expect(exitCode, equals(ExitCode.software.code));
|
||||
verify(
|
||||
() => progress.fail(
|
||||
() => logger.err(
|
||||
any(
|
||||
that: stringContainsInOrder([
|
||||
'Could not find ipa file',
|
||||
@@ -504,7 +663,8 @@ error: exportArchive: No signing certificate "iOS Distribution" found
|
||||
() => codePushClientWrapper.createIosReleaseArtifacts(
|
||||
appId: appId,
|
||||
releaseId: release.id,
|
||||
ipaPath: any(named: 'ipaPath', that: endsWith('.ipa')),
|
||||
xcarchivePath:
|
||||
any(named: 'xcarchivePath', that: endsWith('.xcarchive')),
|
||||
runnerPath: any(named: 'runnerPath', that: endsWith('Runner.app')),
|
||||
),
|
||||
).called(1);
|
||||
@@ -575,7 +735,8 @@ flavors:
|
||||
() => codePushClientWrapper.createIosReleaseArtifacts(
|
||||
appId: appId,
|
||||
releaseId: release.id,
|
||||
ipaPath: any(named: 'ipaPath', that: endsWith('.ipa')),
|
||||
xcarchivePath:
|
||||
any(named: 'xcarchivePath', that: endsWith('.xcarchive')),
|
||||
runnerPath: any(named: 'runnerPath', that: endsWith('Runner.app')),
|
||||
),
|
||||
).called(1);
|
||||
@@ -610,7 +771,8 @@ flavors:
|
||||
() => codePushClientWrapper.createIosReleaseArtifacts(
|
||||
appId: appId,
|
||||
releaseId: release.id,
|
||||
ipaPath: any(named: 'ipaPath', that: endsWith('.ipa')),
|
||||
xcarchivePath:
|
||||
any(named: 'xcarchivePath', that: endsWith('.xcarchive')),
|
||||
runnerPath: any(named: 'runnerPath', that: endsWith('Runner.app')),
|
||||
),
|
||||
).called(1);
|
||||
|
||||
+3
-1
@@ -123,6 +123,7 @@ flutter:
|
||||
}
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(Directory(''));
|
||||
registerFallbackValue(ReleasePlatform.ios);
|
||||
registerFallbackValue(ReleaseStatus.draft);
|
||||
registerFallbackValue(_FakeRelease());
|
||||
@@ -309,7 +310,8 @@ flutter:
|
||||
() => codePushClientWrapper.createIosReleaseArtifacts(
|
||||
appId: appId,
|
||||
releaseId: release.id,
|
||||
ipaPath: any(named: 'ipaPath', that: endsWith('.ipa')),
|
||||
xcarchivePath:
|
||||
any(named: 'xcarchivePath', that: endsWith('.xcarchive')),
|
||||
runnerPath: any(named: 'runnerPath', that: endsWith('Runner.app')),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:io';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:mocktail/mocktail.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/archive_analysis/archive_differ.dart';
|
||||
@@ -92,6 +93,27 @@ void main() {
|
||||
.thenReturn(nativeDiffPrettyString);
|
||||
});
|
||||
|
||||
group('zipAndConfirmUnpatchableDiffsIfNecessary', () {
|
||||
test('zips directory and forwards to confirmUnpatchableDiffsIfNecessary',
|
||||
() async {
|
||||
final tempDir = Directory.systemTemp.createTempSync();
|
||||
final localArtifactDirectory = Directory(
|
||||
p.join(tempDir.path, 'artifact'),
|
||||
)..createSync();
|
||||
|
||||
await runWithOverrides(
|
||||
() => patchDiffChecker.zipAndConfirmUnpatchableDiffsIfNecessary(
|
||||
localArtifactDirectory: localArtifactDirectory,
|
||||
releaseArtifactUrl: releaseArtifactUrl,
|
||||
archiveDiffer: archiveDiffer,
|
||||
force: false,
|
||||
),
|
||||
);
|
||||
|
||||
verify(() => archiveDiffer.changedFiles(any(), any())).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('confirmUnpatchableDiffsIfNecessary', () {
|
||||
test('throws Exception when release artifact fails to download',
|
||||
() async {
|
||||
|
||||
Reference in New Issue
Block a user