feat: ios release with public key bundling (#2143)
Co-authored-by: Bryan Oltman <bryan@shorebird.dev>
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user