feat: ios release with public key bundling (#2143)

Co-authored-by: Bryan Oltman <bryan@shorebird.dev>
This commit is contained in:
Erick
2024-05-23 18:17:08 -03:00
committed by GitHub
parent 47a3361889
commit c3b6f4acee
7 changed files with 179 additions and 18 deletions
@@ -34,6 +34,17 @@ final artifactBuilderRef = create(ArtifactBuilder.new);
/// The [ArtifactBuilder] instance available in the current zone.
ArtifactBuilder get artifactBuilder => read(artifactBuilderRef);
extension on String {
/// Converts this base64-encoded public key into the Map<String, String>:
/// {'SHOREBIRD_PUBLIC_KEY': this}
///
/// SHOREBIRD_PUBLIC_KEY is the name expected by the Shorebird's Flutter tool
///
/// This allow us to just call var?.toPublicKeyEnv() instead of doing
/// a ternary operation to check if the value is null.
Map<String, String> toPublicKeyEnv() => {'SHOREBIRD_PUBLIC_KEY': this};
}
/// @{template artifact_builder}
/// Builds aabs, ipas, and other artifacts produced by `flutter build`.
/// @{endtemplate}
@@ -65,9 +76,7 @@ class ArtifactBuilder {
executable,
arguments,
runInShell: true,
environment: base64PublicKey != null
? {'SHOREBIRD_PUBLIC_KEY': base64PublicKey}
: null,
environment: base64PublicKey?.toPublicKeyEnv(),
);
if (result.exitCode != ExitCode.success.code) {
@@ -126,9 +135,7 @@ class ArtifactBuilder {
executable,
arguments,
runInShell: true,
environment: base64PublicKey != null
? {'SHOREBIRD_PUBLIC_KEY': base64PublicKey}
: null,
environment: base64PublicKey?.toPublicKeyEnv(),
);
if (result.exitCode != ExitCode.success.code) {
@@ -194,6 +201,7 @@ class ArtifactBuilder {
String? flavor,
String? target,
List<String> args = const [],
String? base64PublicKey,
}) async {
return _runShorebirdBuildCommand(() async {
const executable = 'flutter';
@@ -214,6 +222,7 @@ class ArtifactBuilder {
executable,
arguments,
runInShell: true,
environment: base64PublicKey?.toPublicKeyEnv(),
);
if (result.exitCode != ExitCode.success.code) {
@@ -1,5 +1,3 @@
import 'dart:convert';
import 'package:mason_logger/mason_logger.dart';
import 'package:shorebird_cli/src/artifact_builder.dart';
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
@@ -7,7 +5,6 @@ import 'package:shorebird_cli/src/commands/release/release.dart';
import 'package:shorebird_cli/src/commands/release/releaser.dart';
import 'package:shorebird_cli/src/doctor.dart';
import 'package:shorebird_cli/src/extensions/arg_results.dart';
import 'package:shorebird_cli/src/extensions/file.dart';
import 'package:shorebird_cli/src/logger.dart';
import 'package:shorebird_cli/src/platform.dart';
import 'package:shorebird_cli/src/platform/platform.dart';
@@ -63,7 +60,7 @@ class AndroidReleaser extends Releaser {
@override
Future<void> assertArgsAreValid() async {
argResults.file('public-key-path')?.assertExists();
argResults.assertAbsentOrValidPublicKey();
if (generateApk && splitApk) {
logger
..err(
@@ -96,10 +93,7 @@ Please comment and upvote ${link(uri: Uri.parse('https://github.com/shorebirdtec
final File aab;
final publicKeyFile = argResults.file('public-key-path');
final base64PublicKey = publicKeyFile != null
? base64Encode(publicKeyFile.readAsBytesSync())
: null;
final base64PublicKey = argResults.encodedPublicKey;
try {
aab = await artifactBuilder.buildAppBundle(
@@ -9,6 +9,7 @@ import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
import 'package:shorebird_cli/src/commands/release/releaser.dart';
import 'package:shorebird_cli/src/doctor.dart';
import 'package:shorebird_cli/src/executables/xcodebuild.dart';
import 'package:shorebird_cli/src/extensions/arg_results.dart';
import 'package:shorebird_cli/src/logger.dart';
import 'package:shorebird_cli/src/platform.dart';
import 'package:shorebird_cli/src/platform/ios.dart';
@@ -38,6 +39,7 @@ class IosReleaser extends Releaser {
@override
Future<void> assertArgsAreValid() async {
argResults.assertAbsentOrValidPublicKey();
if (argResults.rest.contains('--obfuscate')) {
// Obfuscated releases break patching, so we don't support them.
// See https://github.com/shorebirdtech/shorebird/issues/1619
@@ -98,6 +100,7 @@ class IosReleaser extends Releaser {
final flutterVersionString = await shorebirdFlutter.getVersionAndRevision();
final buildProgress =
logger.progress('Building ipa with Flutter $flutterVersionString');
try {
await artifactBuilder.buildIpa(
codesign: codesign,
@@ -105,6 +108,7 @@ class IosReleaser extends Releaser {
flavor: flavor,
target: target,
args: argResults.forwardedArgs,
base64PublicKey: argResults.encodedPublicKey,
);
buildProgress.complete();
} on ArtifactBuildException catch (error) {
@@ -1,7 +1,9 @@
import 'dart:convert';
import 'dart:io';
import 'package:args/args.dart';
import 'package:collection/collection.dart';
import 'package:shorebird_cli/src/extensions/file.dart';
extension OptionFinder on ArgResults {
/// // Detects flags even when passed to underlying commands via a `--`
@@ -42,6 +44,25 @@ extension OptionFinder on ArgResults {
}
}
const _publicKeyArgName = 'public-key-path';
extension CodeSign on ArgResults {
/// Asserts that either there is no public key argument
/// or that the path received exists.
void assertAbsentOrValidPublicKey() {
file(_publicKeyArgName)?.assertExists();
}
/// Read the public key file and encode it to base64 if any.
String? get encodedPublicKey {
final publicKeyFile = file(_publicKeyArgName);
return publicKeyFile != null
? base64Encode(publicKeyFile.readAsBytesSync())
: null;
}
}
/// Extension on [ArgResults] to provide file related extensions.
extension FileArgs on ArgResults {
/// Returns a [File] from the argument [name] or null if the argument was not
@@ -654,6 +654,52 @@ Either run `flutter pub get` manually, or follow the steps in ${link(uri: Uri.pa
});
});
group('when base64PublicKey is not null', () {
const base64PublicKey = 'base64PublicKey';
setUp(() {
when(
() => shorebirdProcess.run(
'flutter',
[
'build',
'ipa',
'--release',
'--export-options-plist=${exportOptionsPlist.path}',
],
runInShell: any(named: 'runInShell'),
environment: {
'SHOREBIRD_PUBLIC_KEY': base64PublicKey,
},
),
).thenAnswer((_) async => buildProcessResult);
});
test('adds the SHOREBIRD_PUBLIC_KEY to the environment', () async {
await runWithOverrides(
() => builder.buildIpa(
base64PublicKey: base64PublicKey,
),
);
verify(
() => shorebirdProcess.run(
'flutter',
[
'build',
'ipa',
'--release',
'--export-options-plist=${exportOptionsPlist.path}',
],
runInShell: any(named: 'runInShell'),
environment: {
'SHOREBIRD_PUBLIC_KEY': base64PublicKey,
},
),
).called(1);
});
});
group('when export options plist is provided', () {
test('forwards to flutter build', () async {
await runWithOverrides(
@@ -225,7 +225,7 @@ void main() {
final publicKeyFile = File(
p.join(
Directory.systemTemp.createTempSync().path,
'public-key.pem',
'public-key.der',
),
)..writeAsStringSync('public key');
when(() => argResults['public-key-path'])
@@ -244,7 +244,7 @@ void main() {
setUp(() {
when(() => argResults['artifact']).thenReturn('apk');
when(() => argResults['public-key-path'])
.thenReturn('non-existing-key.pem');
.thenReturn('non-existing-key.der');
});
test('logs and exits with usage err', () async {
@@ -253,7 +253,7 @@ void main() {
exitsWithCode(ExitCode.usage),
);
verify(() => logger.err('No file found at non-existing-key.pem'))
verify(() => logger.err('No file found at non-existing-key.der'))
.called(1);
});
});
@@ -432,7 +432,7 @@ void main() {
patchSigningPublicKeyFile = File(
p.join(
Directory.systemTemp.createTempSync().path,
'patch-signing-public-key.pem',
'patch-signing-public-key.der',
),
)..writeAsStringSync('public key');
when(() => argResults['public-key-path'])
@@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:args/args.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:mocktail/mocktail.dart';
@@ -243,6 +245,43 @@ void main() {
);
});
});
group('when a public key is provided and it exists', () {
setUp(() {
final publicKeyFile = File(
p.join(
Directory.systemTemp.createTempSync().path,
'public-key.der',
),
)..writeAsStringSync('public key');
when(() => argResults['public-key-path'])
.thenReturn(publicKeyFile.path);
});
test('returns normally', () async {
expect(
() => runWithOverrides(iosReleaser.assertArgsAreValid),
returnsNormally,
);
});
});
group('when the provided public key is a nonexistent file', () {
setUp(() {
when(() => argResults['public-key-path'])
.thenReturn('non-existing-key.der');
});
test('logs and exits with usage err', () async {
await expectLater(
() => runWithOverrides(iosReleaser.assertArgsAreValid),
exitsWithCode(ExitCode.usage),
);
verify(() => logger.err('No file found at non-existing-key.der'))
.called(1);
});
});
});
group('buildReleaseArtifacts', () {
@@ -282,6 +321,54 @@ void main() {
).thenAnswer((_) async => flutterVersionAndRevision);
});
group('when a patch signing key path is provided', () {
late File patchSigningPublicKeyFile;
setUp(() {
patchSigningPublicKeyFile = File(
p.join(
Directory.systemTemp.createTempSync().path,
'patch-signing-public-key.der',
),
)..writeAsStringSync('public key');
when(() => argResults['public-key-path'])
.thenReturn(patchSigningPublicKeyFile.path);
when(
() => artifactBuilder.buildIpa(
codesign: any(named: 'codesign'),
exportOptionsPlist: any(named: 'exportOptionsPlist'),
flavor: any(named: 'flavor'),
target: any(named: 'target'),
args: any(named: 'args'),
base64PublicKey: any(named: 'base64PublicKey'),
),
).thenAnswer((_) async => File(''));
});
test(
'encodes the patch signing public key and forward it to buildIpa',
() async {
await runWithOverrides(
() => iosReleaser.buildReleaseArtifacts(),
);
verify(
() => artifactBuilder.buildIpa(
codesign: any(named: 'codesign'),
exportOptionsPlist: any(named: 'exportOptionsPlist'),
flavor: any(named: 'flavor'),
target: any(named: 'target'),
args: any(named: 'args'),
base64PublicKey: base64Encode(
patchSigningPublicKeyFile.readAsBytesSync(),
),
),
).called(1);
},
);
});
group('when not codesigning', () {
setUp(() {
when(() => argResults['codesign']).thenReturn(false);