feat(shorebird_cli): iOS linking improvment (improved class table sort) (#2655)

This commit is contained in:
Felix Angelov
2024-12-05 13:25:30 -06:00
committed by GitHub
parent 8fdf60754b
commit 13dcf436dc
20 changed files with 719 additions and 94 deletions
+1 -1
View File
@@ -1 +1 @@
e8206cf2e76e3a4e0c8e062c20bb563cae39a1f1
5a3ba347fd4e2ed6b17077af8ea2aca58bb26747
@@ -7,9 +7,10 @@ import 'package:path/path.dart' as p;
/// A wrapper around a directory that can be zipped.
extension DirectoryArchive on Directory {
/// Copies this directory to a temporary directory and zips it.
Future<File> zipToTempFile() async {
Future<File> zipToTempFile({String? name}) async {
final tempDir = await Directory.systemTemp.createTemp();
final outFile = File(p.join(tempDir.path, '${p.basename(path)}.zip'));
final fileName = name ?? p.basename(path);
final outFile = File(p.join(tempDir.path, '$fileName.zip'));
await Isolate.run(() {
ZipFileEncoder().zipDirectory(this, filename: outFile.path);
});
@@ -357,6 +357,26 @@ class ArtifactManager {
return ipaFiles.single;
}
/// Returns the path to the shorebird release supplement directory on iOS.
///
/// Returns null if there is no supplement directory
/// (e.g. when using older Flutter revisions).
Directory? getIosReleaseSupplementDirectory() {
final projectRoot = shorebirdEnv.getShorebirdProjectRoot()!;
final releaseSupplementDir = Directory(
p.join(projectRoot.path, 'build', 'ios', 'shorebird'),
);
if (!releaseSupplementDir.existsSync()) {
logger.detail(
'No iOS release supplements found at ${releaseSupplementDir.path}',
);
return null;
}
return releaseSupplementDir;
}
/// Name of the App.xcframework generated by `shorebird release ios-framework`
static const String appXcframeworkName = 'App.xcframework';
@@ -616,7 +616,8 @@ aar artifact already exists, continuing...''',
return thinnedArchiveDirectory;
}
/// Uploads a release .xcarchive and .app to the Shorebird server.
/// Uploads a release .xcarchive, .app, and supplementary files to the
/// Shorebird server.
Future<void> createIosReleaseArtifacts({
required String appId,
required int releaseId,
@@ -624,6 +625,7 @@ aar artifact already exists, continuing...''',
required String runnerPath,
required bool isCodesigned,
required String? podfileLockHash,
required String? supplementPath,
}) async {
final createArtifactProgress = logger.progress('Uploading artifacts');
final thinnedArchiveDirectory =
@@ -668,14 +670,40 @@ aar artifact already exists, continuing...''',
);
}
if (supplementPath != null) {
final zippedSupplement = await Directory(supplementPath).zipToTempFile(
name: 'ios_supplement',
);
try {
await codePushClient.createReleaseArtifact(
appId: appId,
releaseId: releaseId,
artifactPath: zippedSupplement.path,
arch: 'ios_supplement',
platform: ReleasePlatform.ios,
hash: sha256.convert(await zippedSupplement.readAsBytes()).toString(),
canSideload: false,
podfileLockHash: podfileLockHash,
);
} catch (error) {
_handleErrorAndExit(
error,
progress: createArtifactProgress,
message: 'Error uploading release supplements: $error',
);
}
}
createArtifactProgress.complete();
}
/// Zips and uploads a release xcframework to the Shorebird server.
/// Zips and uploads a release xcframework and supplementary files to the
/// Shorebird server.
Future<void> createIosFrameworkReleaseArtifacts({
required String appId,
required int releaseId,
required String appFrameworkPath,
required String? supplementPath,
}) async {
final createArtifactProgress = logger.progress('Uploading artifacts');
final appFrameworkDirectory = Directory(appFrameworkPath);
@@ -705,6 +733,30 @@ aar artifact already exists, continuing...''',
);
}
if (supplementPath != null) {
final zippedSupplement = await Directory(supplementPath).zipToTempFile(
name: 'ios_framework_supplement',
);
try {
await codePushClient.createReleaseArtifact(
appId: appId,
releaseId: releaseId,
artifactPath: zippedSupplement.path,
arch: 'ios_framework_supplement',
platform: ReleasePlatform.ios,
hash: sha256.convert(await zippedSupplement.readAsBytes()).toString(),
canSideload: false,
podfileLockHash: null,
);
} catch (error) {
_handleErrorAndExit(
error,
progress: createArtifactProgress,
message: 'Error uploading release supplements: $error',
);
}
}
createArtifactProgress.complete();
}
@@ -105,6 +105,7 @@ class AarPatcher extends Patcher {
required String appId,
required int releaseId,
required File releaseArtifact,
File? supplementArtifact,
}) async {
final releaseArtifacts = await codePushClientWrapper.getReleaseArtifacts(
appId: appId,
@@ -138,6 +138,7 @@ Looked in:
required String appId,
required int releaseId,
required File releaseArtifact,
File? supplementArtifact,
Duration downloadMessageTimeout = const Duration(minutes: 1),
}) async {
final releaseArtifacts = await codePushClientWrapper.getReleaseArtifacts(
@@ -39,6 +39,9 @@ class IosFrameworkPatcher extends Patcher {
required super.target,
});
String get _patchClassTableLinkInfoFile =>
p.join(buildDirectory.path, 'ios', 'shorebird', 'App.ct.link');
String get _vmcodeOutputPath => p.join(buildDirectory.path, 'out.vmcode');
String get _appDillCopyPath => p.join(buildDirectory.path, 'app.dill');
@@ -46,6 +49,9 @@ class IosFrameworkPatcher extends Patcher {
@override
String get primaryReleaseArtifactArch => 'xcframework';
@override
String? get supplementaryReleaseArtifactArch => 'ios_framework_supplement';
@override
ReleaseType get releaseType => ReleaseType.iosFramework;
@@ -145,17 +151,37 @@ class IosFrameworkPatcher extends Patcher {
required String appId,
required int releaseId,
required File releaseArtifact,
File? supplementArtifact,
}) async {
final unzipProgress = logger.progress('Extracting release artifact');
final tempDir = Directory.systemTemp.createTempSync();
await artifactManager.extractZip(
zipFile: releaseArtifact,
outputDirectory: tempDir,
);
final releaseXcframeworkPath = tempDir.path;
late final String releaseXcframeworkPath;
{
final tempDir = Directory.systemTemp.createTempSync();
await artifactManager.extractZip(
zipFile: releaseArtifact,
outputDirectory: tempDir,
);
releaseXcframeworkPath = tempDir.path;
}
unzipProgress
.complete('Extracted release artifact to $releaseXcframeworkPath');
File? releaseClassTableLinkInfoFile;
if (supplementArtifact != null) {
final tempDir = Directory.systemTemp.createTempSync();
await artifactManager.extractZip(
zipFile: supplementArtifact,
outputDirectory: tempDir,
);
releaseClassTableLinkInfoFile = File(p.join(tempDir.path, 'App.ct.link'));
if (!releaseClassTableLinkInfoFile.existsSync()) {
logger.err('Unable to find class table link info file');
throw ProcessExit(ExitCode.software.code);
}
}
unzipProgress.complete(
'Extracted release artifact to $releaseXcframeworkPath',
);
final releaseArtifactFile = File(
p.join(
releaseXcframeworkPath,
@@ -174,6 +200,22 @@ class IosFrameworkPatcher extends Patcher {
);
final useLinker = AotTools.usesLinker(shorebirdEnv.flutterRevision);
if (useLinker) {
// If we're using a newer version of the linker, we need to also copy the
// necessary class table link information alongside the snapshots.
if (releaseClassTableLinkInfoFile != null) {
// Copy the release's class table link info file next to the release
// snapshot so that it can be used to generate a patch.
releaseClassTableLinkInfoFile.copySync(
p.join(releaseArtifactFile.parent.path, 'App.ct.link'),
);
// Copy the patch's class table link info file to the build directory
// so that it can be used to generate a patch.
File(_patchClassTableLinkInfoFile).copySync(
p.join(buildDirectory.path, 'out.ct.link'),
);
}
await _runLinker(
aotSnapshot: aotSnapshotFile,
releaseArtifact: releaseArtifactFile,
@@ -47,6 +47,9 @@ class IosPatcher extends Patcher {
required super.target,
});
String get _patchClassTableLinkInfoPath =>
p.join(buildDirectory.path, 'ios', 'shorebird', 'App.ct.link');
String get _aotOutputPath => p.join(buildDirectory.path, 'out.aot');
String get _vmcodeOutputPath => p.join(buildDirectory.path, 'out.vmcode');
@@ -85,6 +88,9 @@ class IosPatcher extends Patcher {
@override
String get primaryReleaseArtifactArch => 'xcarchive';
@override
String? get supplementaryReleaseArtifactArch => 'ios_supplement';
@override
Future<void> assertPreconditions() async {
try {
@@ -229,6 +235,7 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
required String appId,
required int releaseId,
required File releaseArtifact,
File? supplementArtifact,
}) async {
// Verify that we have built a patch .xcarchive
if (artifactManager.getXcarchiveDirectory()?.path == null) {
@@ -237,12 +244,31 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
}
final unzipProgress = logger.progress('Extracting release artifact');
final tempDir = Directory.systemTemp.createTempSync();
await artifactManager.extractZip(
zipFile: releaseArtifact,
outputDirectory: tempDir,
);
final releaseXcarchivePath = tempDir.path;
late final String releaseXcarchivePath;
{
final tempDir = Directory.systemTemp.createTempSync();
await artifactManager.extractZip(
zipFile: releaseArtifact,
outputDirectory: tempDir,
);
releaseXcarchivePath = tempDir.path;
}
File? releaseClassTableLinkInfoFile;
if (supplementArtifact != null) {
final tempDir = Directory.systemTemp.createTempSync();
await artifactManager.extractZip(
zipFile: supplementArtifact,
outputDirectory: tempDir,
);
releaseClassTableLinkInfoFile = File(p.join(tempDir.path, 'App.ct.link'));
if (!releaseClassTableLinkInfoFile.existsSync()) {
logger.err('Unable to find class table link info file');
throw ProcessExit(ExitCode.software.code);
}
}
unzipProgress.complete();
final appDirectory = artifactManager.getIosAppDirectory(
@@ -263,6 +289,22 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
final useLinker = AotTools.usesLinker(shorebirdEnv.flutterRevision);
if (useLinker) {
// If we're using a newer version of the linker, we need to also copy the
// necessary class table link information alongside the snapshots.
if (releaseClassTableLinkInfoFile != null) {
// Copy the release's class table link info file next to the release
// snapshot so that it can be used to generate a patch.
releaseClassTableLinkInfoFile.copySync(
p.join(releaseArtifactFile.parent.path, 'App.ct.link'),
);
// Copy the patch's class table link info file to the build directory
// so that it can be used to generate a patch.
File(_patchClassTableLinkInfoPath).copySync(
p.join(buildDirectory.path, 'out.ct.link'),
);
}
final (:exitCode, :linkPercentage) = await _runLinker(
releaseArtifact: releaseArtifactFile,
kernelFile: File(_appDillCopyPath),
@@ -283,11 +283,24 @@ NOTE: this is ${styleBold.wrap('not')} recommended. Asset changes cannot be incl
platform: patcher.releaseType.releasePlatform,
);
final releaseArchive = await downloadPrimaryReleaseArtifact(
final supplementalArtifact =
patcher.supplementaryReleaseArtifactArch != null
? await codePushClientWrapper.getOptionalReleaseArtifact(
appId: appId,
releaseId: release.id,
arch: patcher.supplementaryReleaseArtifactArch!,
platform: patcher.releaseType.releasePlatform,
)
: null;
final releaseArchive = await downloadReleaseArtifact(
releaseArtifact: releaseArtifact,
patcher: patcher,
);
final supplementArchive = supplementalArtifact != null
? await downloadReleaseArtifact(releaseArtifact: supplementalArtifact)
: null;
final releaseFlutterShorebirdEnv = shorebirdEnv.copyWith(
flutterRevisionOverride: release.flutterRevision,
);
@@ -313,6 +326,7 @@ NOTE: this is ${styleBold.wrap('not')} recommended. Asset changes cannot be incl
appId: appId,
releaseId: release.id,
releaseArtifact: releaseArchive,
supplementArtifact: supplementArchive,
);
final dryRun = results['dry-run'] == true;
@@ -478,15 +492,14 @@ ${summary.join('\n')}
}
}
Future<File> downloadPrimaryReleaseArtifact({
Future<File> downloadReleaseArtifact({
required ReleaseArtifact releaseArtifact,
required Patcher patcher,
}) async {
final File artifactFile;
try {
artifactFile = await artifactManager.downloadWithProgressUpdates(
Uri.parse(releaseArtifact.url),
message: 'Downloading ${patcher.primaryReleaseArtifactArch}',
message: 'Downloading ${releaseArtifact.arch}',
);
} catch (_) {
throw ProcessExit(ExitCode.software.code);
@@ -495,3 +508,24 @@ ${summary.join('\n')}
return artifactFile;
}
}
extension on CodePushClientWrapper {
Future<ReleaseArtifact?> getOptionalReleaseArtifact({
required String appId,
required int releaseId,
required String arch,
required ReleasePlatform platform,
}) async {
try {
final artifact = await getReleaseArtifact(
appId: appId,
releaseId: releaseId,
arch: arch,
platform: platform,
);
return artifact;
} on CodePushNotFoundException catch (_) {
return null;
}
}
}
@@ -62,6 +62,9 @@ ${iOSLinkPercentageUrl.toLink()}
/// For example, 'aab' for Android, 'xcarchive' for iOS.
String get primaryReleaseArtifactArch;
/// The identifier used for any supplementary release artifacts.
String? get supplementaryReleaseArtifactArch => null;
/// The root directory of the current project.
Directory get projectRoot => shorebirdEnv.getShorebirdProjectRoot()!;
@@ -91,6 +94,7 @@ ${iOSLinkPercentageUrl.toLink()}
required String appId,
required int releaseId,
required File releaseArtifact,
File? supplementArtifact,
});
/// Updates the provided metadata to include patcher-specific fields.
@@ -144,6 +144,7 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
appId: appId,
releaseId: release.id,
appFrameworkPath: p.join(releaseDirectory.path, 'App.xcframework'),
supplementPath: artifactManager.getIosReleaseSupplementDirectory()?.path,
);
}
@@ -187,6 +187,7 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
.path,
isCodesigned: codesign,
podfileLockHash: podfileLockHash,
supplementPath: artifactManager.getIosReleaseSupplementDirectory()?.path,
);
}
@@ -806,5 +806,45 @@ void main() {
);
});
});
group('getIosReleaseSupplementDirectory', () {
group('when the directory does not exist', () {
test('returns null', () {
expect(
runWithOverrides(artifactManager.getIosReleaseSupplementDirectory),
isNull,
);
});
});
group('when the directory exists', () {
setUp(() {
Directory(
p.join(
projectRoot.path,
'build',
'ios',
'shorebird',
),
).createSync(recursive: true);
});
test('returns path to the directory', () {
expect(
runWithOverrides(
artifactManager.getIosReleaseSupplementDirectory,
)?.path,
equals(
p.join(
projectRoot.path,
'build',
'ios',
'shorebird',
),
),
);
});
});
});
});
}
@@ -1709,6 +1709,7 @@ You can manage this release in the ${link(uri: uri, message: 'Shorebird Console'
const podfileLockHash = 'podfile-lock-hash';
final xcarchivePath = p.join('path', 'to', 'app.xcarchive');
final runnerPath = p.join('path', 'to', 'runner.app');
final releaseSupplementPath = p.join('path', 'to', 'supplement');
void setUpProjectRoot({String? flavor}) {
Directory(
@@ -1717,6 +1718,9 @@ You can manage this release in the ${link(uri: uri, message: 'Shorebird Console'
Directory(
p.join(projectRoot.path, runnerPath),
).createSync(recursive: true);
Directory(
p.join(projectRoot.path, releaseSupplementPath),
).createSync(recursive: true);
}
setUp(() {
@@ -1761,6 +1765,7 @@ You can manage this release in the ${link(uri: uri, message: 'Shorebird Console'
runnerPath: p.join(projectRoot.path, runnerPath),
isCodesigned: true,
podfileLockHash: podfileLockHash,
supplementPath: p.join(projectRoot.path, releaseSupplementPath),
),
),
exitsWithCode(ExitCode.software),
@@ -1798,6 +1803,7 @@ You can manage this release in the ${link(uri: uri, message: 'Shorebird Console'
runnerPath: p.join(projectRoot.path, runnerPath),
isCodesigned: false,
podfileLockHash: podfileLockHash,
supplementPath: p.join(projectRoot.path, releaseSupplementPath),
),
),
exitsWithCode(ExitCode.software),
@@ -1835,6 +1841,45 @@ You can manage this release in the ${link(uri: uri, message: 'Shorebird Console'
runnerPath: p.join(projectRoot.path, runnerPath),
isCodesigned: false,
podfileLockHash: podfileLockHash,
supplementPath: p.join(projectRoot.path, releaseSupplementPath),
),
),
exitsWithCode(ExitCode.software),
);
verify(() => progress.fail(any(that: contains(error)))).called(1);
});
test('exits with code 70 when supplement artifact creation fails',
() async {
const error = 'something went wrong';
when(
() => codePushClient.createReleaseArtifact(
appId: any(named: 'appId'),
artifactPath: any(
named: 'artifactPath',
that: endsWith('ios_supplement.zip'),
),
releaseId: any(named: 'releaseId'),
arch: any(named: 'arch'),
platform: any(named: 'platform'),
hash: any(named: 'hash'),
canSideload: any(named: 'canSideload'),
podfileLockHash: any(named: 'podfileLockHash'),
),
).thenThrow(error);
setUpProjectRoot();
await expectLater(
() async => runWithOverrides(
() async => codePushClientWrapper.createIosReleaseArtifacts(
appId: app.appId,
releaseId: releaseId,
xcarchivePath: p.join(projectRoot.path, xcarchivePath),
runnerPath: p.join(projectRoot.path, runnerPath),
isCodesigned: false,
podfileLockHash: podfileLockHash,
supplementPath: p.join(projectRoot.path, releaseSupplementPath),
),
),
exitsWithCode(ExitCode.software),
@@ -1866,6 +1911,7 @@ You can manage this release in the ${link(uri: uri, message: 'Shorebird Console'
runnerPath: p.join(projectRoot.path, runnerPath),
isCodesigned: true,
podfileLockHash: podfileLockHash,
supplementPath: p.join(projectRoot.path, releaseSupplementPath),
),
);
@@ -1891,44 +1937,35 @@ You can manage this release in the ${link(uri: uri, message: 'Shorebird Console'
group('createIosFrameworkReleaseArtifacts', () {
final frameworkPath = p.join('path', 'to', 'App.xcframework');
final releaseSupplementPath = p.join('path', 'to', 'supplement');
void setUpProjectRoot({String? flavor}) {
Directory(
p.join(projectRoot.path, frameworkPath),
).createSync(recursive: true);
Directory(
p.join(projectRoot.path, releaseSupplementPath),
).createSync(recursive: true);
}
test(
'exits with code 70 when creating xcframework artifact fails',
() async {
when(
() => codePushClient.createReleaseArtifact(
artifactPath: any(named: 'artifactPath'),
appId: any(named: 'appId'),
releaseId: any(named: 'releaseId'),
arch: any(named: 'arch'),
platform: any(named: 'platform'),
hash: any(named: 'hash'),
canSideload: any(named: 'canSideload'),
podfileLockHash: any(named: 'podfileLockHash'),
),
).thenThrow(Exception('oh no'));
setUpProjectRoot();
setUp(() {
when(
() => codePushClient.createReleaseArtifact(
appId: any(named: 'appId'),
artifactPath: any(named: 'artifactPath'),
releaseId: any(named: 'releaseId'),
arch: any(named: 'arch'),
platform: any(named: 'platform'),
hash: any(named: 'hash'),
canSideload: any(named: 'canSideload'),
podfileLockHash: any(named: 'podfileLockHash'),
),
).thenAnswer((_) async {});
setUpProjectRoot();
});
await expectLater(
() async => runWithOverrides(
() => codePushClientWrapper.createIosFrameworkReleaseArtifacts(
appId: app.appId,
releaseId: releaseId,
appFrameworkPath: p.join(projectRoot.path, frameworkPath),
),
),
exitsWithCode(ExitCode.software),
);
},
);
test('completes successfully when release artifact is created', () async {
test('exits with code 70 when creating xcframework artifact fails',
() async {
when(
() => codePushClient.createReleaseArtifact(
artifactPath: any(named: 'artifactPath'),
@@ -1940,15 +1977,64 @@ You can manage this release in the ${link(uri: uri, message: 'Shorebird Console'
canSideload: any(named: 'canSideload'),
podfileLockHash: any(named: 'podfileLockHash'),
),
).thenAnswer((_) async {});
setUpProjectRoot();
).thenThrow(Exception('oh no'));
await expectLater(
() async => runWithOverrides(
() => codePushClientWrapper.createIosFrameworkReleaseArtifacts(
appId: app.appId,
releaseId: releaseId,
appFrameworkPath: p.join(projectRoot.path, frameworkPath),
supplementPath: null,
),
),
exitsWithCode(ExitCode.software),
);
});
test('exits with code 70 when supplement artifact creation fails',
() async {
const error = 'something went wrong';
when(
() => codePushClient.createReleaseArtifact(
appId: any(named: 'appId'),
artifactPath: any(
named: 'artifactPath',
that: endsWith('ios_framework_supplement.zip'),
),
releaseId: any(named: 'releaseId'),
arch: any(named: 'arch'),
platform: any(named: 'platform'),
hash: any(named: 'hash'),
canSideload: any(named: 'canSideload'),
podfileLockHash: any(named: 'podfileLockHash'),
),
).thenThrow(error);
await expectLater(
() async => runWithOverrides(
() async =>
codePushClientWrapper.createIosFrameworkReleaseArtifacts(
appId: app.appId,
releaseId: releaseId,
appFrameworkPath: p.join(projectRoot.path, frameworkPath),
supplementPath: p.join(projectRoot.path, releaseSupplementPath),
),
),
exitsWithCode(ExitCode.software),
);
verify(() => progress.fail(any(that: contains(error)))).called(1);
});
test('completes successfully when release artifact is created', () async {
await expectLater(
runWithOverrides(
() => codePushClientWrapper.createIosFrameworkReleaseArtifacts(
appId: app.appId,
releaseId: releaseId,
appFrameworkPath: p.join(projectRoot.path, frameworkPath),
supplementPath: null,
),
),
completes,
@@ -143,6 +143,15 @@ void main() {
});
});
group('supplementaryReleaseArtifactArch', () {
test('is "ios_framework_supplement"', () {
expect(
patcher.supplementaryReleaseArtifactArch,
'ios_framework_supplement',
);
});
});
group('releaseType', () {
test('is ReleaseType.iosFramework', () {
expect(patcher.releaseType, ReleaseType.iosFramework);
@@ -499,13 +508,14 @@ void main() {
canSideload: true,
);
late File releaseArtifactFile;
late File supplementArtifactFile;
void setUpProjectRootArtifacts() {
File(p.join(projectRoot.path, 'build', elfAotSnapshotFileName))
.createSync(
recursive: true,
);
Directory(
File(
p.join(
projectRoot.path,
'build',
@@ -513,13 +523,23 @@ void main() {
'framework',
'Release',
'App.xcframework',
'ios-arm64',
'App.framework',
'App',
),
).createSync(
recursive: true,
);
).createSync(recursive: true);
File(
p.join(projectRoot.path, 'build', linkFileName),
).createSync(recursive: true);
File(
p.join(
projectRoot.path,
'build',
'ios',
'shorebird',
'App.ct.link',
),
).createSync(recursive: true);
}
setUp(() {
@@ -529,6 +549,12 @@ void main() {
'release.xcframework',
),
)..createSync(recursive: true);
supplementArtifactFile = File(
p.join(
Directory.systemTemp.createTempSync().path,
'ios_framework_supplement.zip',
),
)..createSync(recursive: true);
when(
() => codePushClientWrapper.getReleaseArtifact(
@@ -828,6 +854,92 @@ void main() {
),
);
});
group('when class table link info is not present', () {
setUp(() {
when(
() => artifactManager.extractZip(
zipFile: supplementArtifactFile,
outputDirectory: any(named: 'outputDirectory'),
),
).thenAnswer((invocation) async {});
});
test('exits with code 70', () async {
await expectLater(
() => runWithOverrides(
() => patcher.createPatchArtifacts(
appId: appId,
releaseId: releaseId,
releaseArtifact: releaseArtifactFile,
supplementArtifact: supplementArtifactFile,
),
),
exitsWithCode(ExitCode.software),
);
verify(
() => logger.err(
'Unable to find class table link info file',
),
).called(1);
});
});
group('when class table link info is present', () {
setUp(() {
when(
() => artifactManager.extractZip(
zipFile: releaseArtifactFile,
outputDirectory: any(named: 'outputDirectory'),
),
).thenAnswer((invocation) async {
final outDir = invocation.namedArguments[#outputDirectory]
as Directory;
File(
p.join(
outDir.path,
'ios-arm64',
'App.framework',
'App',
),
).createSync(recursive: true);
});
when(
() => artifactManager.extractZip(
zipFile: supplementArtifactFile,
outputDirectory: any(named: 'outputDirectory'),
),
).thenAnswer((invocation) async {
final outDir = invocation.namedArguments[#outputDirectory]
as Directory;
File(
p.join(outDir.path, 'App.ct.link'),
).createSync(recursive: true);
});
});
test('returns linked patch artifact in patch bundle', () async {
final patchBundle = await runWithOverrides(
() => patcher.createPatchArtifacts(
appId: appId,
releaseId: releaseId,
releaseArtifact: releaseArtifactFile,
supplementArtifact: supplementArtifactFile,
),
);
expect(patchBundle, hasLength(1));
expect(
patchBundle[Arch.arm64],
isA<PatchArtifactBundle>().having(
(b) => b.path,
'path',
endsWith(diffPath),
),
);
});
});
});
});
@@ -158,6 +158,12 @@ void main() {
});
});
group('supplementaryReleaseArtifactArch', () {
test('is "ios_supplement"', () {
expect(patcher.supplementaryReleaseArtifactArch, 'ios_supplement');
});
});
group('releaseType', () {
test('is ReleaseType.ios', () {
expect(patcher.releaseType, ReleaseType.ios);
@@ -818,12 +824,12 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
canSideload: true,
);
late File releaseArtifactFile;
late File supplementArtifactFile;
void setUpProjectRootArtifacts() {
File(p.join(projectRoot.path, 'build', elfAotSnapshotFileName))
.createSync(
recursive: true,
);
File(
p.join(projectRoot.path, 'build', elfAotSnapshotFileName),
).createSync(recursive: true);
Directory(
p.join(
projectRoot.path,
@@ -833,10 +839,8 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
'Release',
'App.xcframework',
),
).createSync(
recursive: true,
);
Directory(
).createSync(recursive: true);
File(
p.join(
projectRoot.path,
'build',
@@ -847,10 +851,20 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
'Products',
'Applications',
'Runner.app',
'Frameworks',
'App.framework',
'App',
),
).createSync(
recursive: true,
);
).createSync(recursive: true);
File(
p.join(
projectRoot.path,
'build',
'ios',
'shorebird',
'App.ct.link',
),
).createSync(recursive: true);
File(
p.join(projectRoot.path, 'build', linkFileName),
).createSync(recursive: true);
@@ -863,6 +877,12 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
'release.xcarchive',
),
)..createSync(recursive: true);
supplementArtifactFile = File(
p.join(
Directory.systemTemp.createTempSync().path,
'ios_supplement.zip',
),
)..createSync(recursive: true);
when(
() => codePushClientWrapper.getReleaseArtifact(
@@ -1269,6 +1289,75 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
);
});
group('when class table link info is not present', () {
setUp(() {
when(
() => artifactManager.extractZip(
zipFile: supplementArtifactFile,
outputDirectory: any(named: 'outputDirectory'),
),
).thenAnswer((invocation) async {});
});
test('exits with code 70', () async {
await expectLater(
() => runWithOverrides(
() => patcher.createPatchArtifacts(
appId: appId,
releaseId: releaseId,
releaseArtifact: releaseArtifactFile,
supplementArtifact: supplementArtifactFile,
),
),
exitsWithCode(ExitCode.software),
);
verify(
() => logger.err(
'Unable to find class table link info file',
),
).called(1);
});
});
group('when class table link info is present', () {
setUp(() {
when(
() => artifactManager.extractZip(
zipFile: supplementArtifactFile,
outputDirectory: any(named: 'outputDirectory'),
),
).thenAnswer((invocation) async {
final outDir = invocation.namedArguments[#outputDirectory]
as Directory;
File(
p.join(outDir.path, 'App.ct.link'),
).createSync(recursive: true);
});
});
test('returns linked patch artifact in patch bundle', () async {
final patchBundle = await runWithOverrides(
() => patcher.createPatchArtifacts(
appId: appId,
releaseId: releaseId,
releaseArtifact: releaseArtifactFile,
supplementArtifact: supplementArtifactFile,
),
);
expect(patchBundle, hasLength(1));
expect(
patchBundle[Arch.arm64],
isA<PatchArtifactBundle>().having(
(b) => b.path,
'path',
endsWith(diffPath),
),
);
});
});
group('when isLinkDebugInfoSupported is true', () {
setUp(() {
when(
@@ -24,7 +24,6 @@ import 'package:shorebird_cli/src/shorebird_env.dart';
import 'package:shorebird_cli/src/shorebird_flutter.dart';
import 'package:shorebird_cli/src/shorebird_validator.dart';
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
import 'package:test/test.dart';
import '../../fakes.dart';
@@ -95,6 +94,17 @@ void main() {
podfileLockHash: null,
canSideload: true,
);
const supplementArtifact = ReleaseArtifact(
id: 0,
releaseId: 0,
arch: arch,
platform: releasePlatform,
hash: '#',
size: 422,
url: 'https://example.com/supplement.zip',
podfileLockHash: null,
canSideload: false,
);
late AotTools aotTools;
late ArgResults argResults;
@@ -225,6 +235,14 @@ void main() {
platform: ReleasePlatform.android,
),
).thenAnswer((_) async => aabArtifact);
when(
() => codePushClientWrapper.getReleaseArtifact(
appId: any(named: 'appId'),
releaseId: any(named: 'releaseId'),
arch: 'supplement',
platform: ReleasePlatform.android,
),
).thenAnswer((_) async => supplementArtifact);
when(
() => logger.chooseOne<Release>(
@@ -253,6 +271,7 @@ void main() {
appId: any(named: 'appId'),
releaseId: any(named: 'releaseId'),
releaseArtifact: any(named: 'releaseArtifact'),
supplementArtifact: any(named: 'supplementArtifact'),
),
).thenAnswer((_) async => patchArtifactBundles);
when(
@@ -427,32 +446,84 @@ void main() {
},
);
group(
'when given an existing public key and nonexistent private key',
() {
test('fails and logs the err', () async {
when(
() => argResults.wasParsed(CommonArguments.privateKeyArg.name),
).thenReturn(false);
when(
() => argResults.wasParsed(CommonArguments.publicKeyArg.name),
).thenReturn(true);
when(
() => argResults[CommonArguments.publicKeyArg.name],
).thenReturn(createTempFile('public.pem').path);
group('when given an existing public key and nonexistent private key',
() {
test('fails and logs the err', () async {
when(
() => argResults.wasParsed(CommonArguments.privateKeyArg.name),
).thenReturn(false);
when(
() => argResults.wasParsed(CommonArguments.publicKeyArg.name),
).thenReturn(true);
when(
() => argResults[CommonArguments.publicKeyArg.name],
).thenReturn(createTempFile('public.pem').path);
await expectLater(
runWithOverrides(() => command.createPatch(patcher)),
exitsWithCode(ExitCode.usage),
);
await expectLater(
runWithOverrides(() => command.createPatch(patcher)),
exitsWithCode(ExitCode.usage),
);
verify(
() => logger.err(
'Both public and private keys must be provided.',
),
).called(1);
});
});
group('when a supplemental release artifact exists', () {
setUp(() {
when(
() => patcher.supplementaryReleaseArtifactArch,
).thenReturn('supplement');
});
test('downloads the supplemental release artifact', () async {
await runWithOverrides(() => command.createPatch(patcher));
verify(
() => codePushClientWrapper.getReleaseArtifact(
appId: appId,
releaseId: release.id,
arch: 'supplement',
platform: releasePlatform,
),
).called(1);
verify(
() => patcher.createPatchArtifacts(
appId: appId,
releaseId: release.id,
releaseArtifact: any(named: 'releaseArtifact'),
supplementArtifact: any(named: 'supplementArtifact'),
),
).called(1);
});
group('when the artifact is not found', () {
setUp(() {
when(
() => codePushClientWrapper.getReleaseArtifact(
appId: appId,
releaseId: release.id,
arch: 'supplement',
platform: releasePlatform,
),
).thenThrow(CodePushNotFoundException(message: 'Not found'));
});
test('gracefully continues to create patch', () async {
await runWithOverrides(() => command.createPatch(patcher));
verify(
() => logger.err(
'Both public and private keys must be provided.',
() => patcher.createPatchArtifacts(
appId: appId,
releaseId: release.id,
releaseArtifact: any(named: 'releaseArtifact'),
supplementArtifact: any(named: 'supplementArtifact'),
),
).called(1);
});
},
);
});
});
});
});
@@ -518,6 +589,10 @@ void main() {
).thenReturn(DeploymentTrack.staging.channel);
});
test('isStaging returns true', () {
expect(command.isStaging, isTrue);
});
test('logs correct summary', () async {
final expectedSummary = [
'''📱 App: ${lightCyan.wrap(appDisplayName)} ${lightCyan.wrap('($appId)')}''',
@@ -36,6 +36,20 @@ void main() {
});
});
group('supplementaryReleaseArtifactArch', () {
test('defaults to null', () {
expect(
_TestPatcher(
argParser: MockArgParser(),
argResults: MockArgResults(),
flavor: null,
target: null,
).supplementaryReleaseArtifactArch,
isNull,
);
});
});
group('assertArgsAreValid', () {
test('has no validations by default', () {
expect(
@@ -261,6 +275,7 @@ class _TestPatcher extends Patcher {
required String appId,
required int releaseId,
required File releaseArtifact,
File? supplementArtifact,
}) {
throw UnimplementedError();
}
@@ -399,6 +399,7 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
appId: any(named: 'appId'),
releaseId: any(named: 'releaseId'),
appFrameworkPath: any(named: 'appFrameworkPath'),
supplementPath: any(named: 'supplementPath'),
),
).thenAnswer((_) async {});
});
@@ -420,6 +421,7 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''',
'release',
ArtifactManager.appXcframeworkName,
),
supplementPath: null,
),
).called(1);
});
@@ -677,6 +677,7 @@ To change the version of this release, change your app's version in your pubspec
late Directory xcarchiveDirectory;
late Directory iosAppDirectory;
late Directory supplementDirectory;
late File podfileLockFile;
setUp(() {
@@ -684,6 +685,7 @@ To change the version of this release, change your app's version in your pubspec
xcarchiveDirectory = Directory.systemTemp.createTempSync();
iosAppDirectory = Directory.systemTemp.createTempSync();
supplementDirectory = Directory.systemTemp.createTempSync();
podfileLockFile = File(
p.join(
Directory.systemTemp.createTempSync().path,
@@ -699,6 +701,9 @@ To change the version of this release, change your app's version in your pubspec
xcarchiveDirectory: any(named: 'xcarchiveDirectory'),
),
).thenReturn(iosAppDirectory);
when(
() => artifactManager.getIosReleaseSupplementDirectory(),
).thenReturn(supplementDirectory);
when(
() => codePushClientWrapper.createIosReleaseArtifacts(
appId: any(named: 'appId'),
@@ -707,6 +712,7 @@ To change the version of this release, change your app's version in your pubspec
runnerPath: any(named: 'runnerPath'),
isCodesigned: any(named: 'isCodesigned'),
podfileLockHash: any(named: 'podfileLockHash'),
supplementPath: any(named: 'supplementPath'),
),
).thenAnswer((_) async => {});
when(() => shorebirdEnv.podfileLockFile).thenReturn(podfileLockFile);
@@ -728,7 +734,8 @@ To change the version of this release, change your app's version in your pubspec
runnerPath: iosAppDirectory.path,
isCodesigned: codesign,
podfileLockHash:
sha256.convert(utf8.encode(podfileLockContent)).toString(),
'${sha256.convert(utf8.encode(podfileLockContent))}',
supplementPath: supplementDirectory.path,
),
).called(1);
});