Skip to content

Commit

Permalink
feat(shorebird_cli): add shorebird release get-apk command (#2586)
Browse files Browse the repository at this point in the history
  • Loading branch information
bryanoltman authored Oct 28, 2024
1 parent 5d33b25 commit 1483151
Show file tree
Hide file tree
Showing 10 changed files with 702 additions and 3 deletions.
1 change: 1 addition & 0 deletions packages/shorebird_cli/lib/src/commands/commands.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ export 'patch/patch.dart';
export 'patches/patches.dart';
export 'preview_command.dart';
export 'release/release.dart';
export 'releases/releases.dart';
export 'run_command.dart';
export 'upgrade_command.dart';
5 changes: 3 additions & 2 deletions packages/shorebird_cli/lib/src/commands/preview_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'package:path/path.dart' as p;
import 'package:shorebird_cli/src/artifact_manager.dart';
import 'package:shorebird_cli/src/cache.dart';
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
import 'package:shorebird_cli/src/common_arguments.dart';
import 'package:shorebird_cli/src/deployment_track.dart';
import 'package:shorebird_cli/src/executables/devicectl/apple_device.dart';
import 'package:shorebird_cli/src/executables/executables.dart';
Expand Down Expand Up @@ -43,8 +44,8 @@ class PreviewCommand extends ShorebirdCommand {
help: 'The ID of the app to preview the release for.',
)
..addOption(
'release-version',
help: 'The version of the release (e.g. "1.0.0").',
CommonArguments.releaseVersionArg.name,
help: CommonArguments.releaseVersionArg.description,
)
..addOption(
'platform',
Expand Down
187 changes: 187 additions & 0 deletions packages/shorebird_cli/lib/src/commands/releases/get_apks_command.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import 'dart:io';

import 'package:archive/archive_io.dart';
import 'package:collection/collection.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:path/path.dart' as p;
import 'package:shorebird_cli/src/artifact_manager.dart';
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
import 'package:shorebird_cli/src/common_arguments.dart';
import 'package:shorebird_cli/src/config/config.dart';
import 'package:shorebird_cli/src/executables/bundletool.dart';
import 'package:shorebird_cli/src/extensions/arg_results.dart';
import 'package:shorebird_cli/src/logging/shorebird_logger.dart';
import 'package:shorebird_cli/src/shorebird_command.dart';
import 'package:shorebird_cli/src/shorebird_env.dart';
import 'package:shorebird_cli/src/shorebird_validator.dart';
import 'package:shorebird_cli/src/third_party/flutter_tools/lib/flutter_tools.dart';
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';

/// {@template get_apk_command}
/// Generates an APK for the release with the specified version.
/// {@endtemplate}
class GetApksCommand extends ShorebirdCommand {
/// {@macro get_apk_command}
GetApksCommand() {
argParser
..addOption(
CommonArguments.releaseVersionArg.name,
help: 'The release version to generate apks for',
)
..addOption(
CommonArguments.flavorArg.name,
help: 'The build flavor to generate an apks for',
)
..addOption(
'out',
abbr: 'o',
help: 'The output directory for the generated apks',
)
..addFlag(
'universal',
defaultsTo: true,
help: 'Whether to generate a universal apk. Defaults to true.',
);
}

@override
String get name => 'get-apks';

@override
String get description =>
'Generates apk(s) for the specified release version';

/// The shorebird app ID for the current project.
String get appId => shorebirdEnv.getShorebirdYaml()!.getAppId(flavor: flavor);

/// The build flavor, if provided.
late String? flavor = results.findOption(
CommonArguments.flavorArg.name,
argParser: argParser,
);

/// The output directory path for the generated apks. Defaults to the
/// project's build directory if not provided.
late String? outDirectoryArg = results.findOption(
'out',
argParser: argParser,
);

@override
Future<int> run() async {
try {
await shorebirdValidator.validatePreconditions(
checkUserIsAuthenticated: true,
checkShorebirdInitialized: true,
);
} on PreconditionFailedException catch (error) {
return error.exitCode.code;
}

final Release release;
if (results.wasParsed(CommonArguments.releaseVersionArg.name)) {
final releaseVersion =
results[CommonArguments.releaseVersionArg.name] as String;
release = await codePushClientWrapper.getRelease(
appId: appId,
releaseVersion: releaseVersion,
);
} else {
release = await _promptForRelease();
}

final releaseArtifact = await codePushClientWrapper.getReleaseArtifact(
appId: appId,
releaseId: release.id,
arch: 'aab',
platform: ReleasePlatform.android,
);

final aabFile = await _downloadAab(releaseArtifact: releaseArtifact);
final apksFile = File(
p.join(
Directory.systemTemp.createTempSync().path,
'${appId}_${release.version}.apks',
),
);

final buildApksProgress = logger.progress(
'Building apks for release ${release.version} (app: $appId)',
);
try {
await bundletool.buildApks(
bundle: aabFile.path,
output: apksFile.path,
universal: results['universal'] as bool,
);
buildApksProgress.complete();
} catch (error) {
buildApksProgress.fail('$error');
return ExitCode.software.code;
}

final apksZipFile = apksFile.renameSync('${apksFile.path}.zip');

final Directory outputDirectory;
if (outDirectoryArg != null) {
outputDirectory = Directory(outDirectoryArg!);
if (!outputDirectory.existsSync()) {
outputDirectory.createSync(recursive: true);
}
} else {
// The output of `flutter build apk` is build/app/outputs/flutter-apk,
// so we move the generated apk to build/app/outputs/shorebird-apk.
outputDirectory = Directory(
p.join(
shorebirdEnv.getShorebirdProjectRoot()!.path,
'build',
'app',
'outputs',
'shorebird-apk',
),
)..createSync(recursive: true);
}

await extractFileToDisk(
apksZipFile.path,
outputDirectory.path,
);

logger.info('apk(s) generated at ${lightCyan.wrap(outputDirectory.path)}');
return ExitCode.success.code;
}

Future<Release> _promptForRelease() async {
final releases = await codePushClientWrapper.getReleases(
appId: appId,
sideloadableOnly: true,
);

if (releases.isEmpty) {
logger.err('No releases found for app $appId');
throw ProcessExit(ExitCode.usage.code);
}

return logger.chooseOne<Release>(
'Which release would you like to generate an apk for?',
choices: releases.sortedBy((r) => r.createdAt).reversed.toList(),
display: (r) => r.version,
);
}

Future<File> _downloadAab({
required ReleaseArtifact releaseArtifact,
}) async {
final File artifactFile;
try {
artifactFile = await artifactManager.downloadWithProgressUpdates(
Uri.parse(releaseArtifact.url),
message: 'Downloading aab',
);
} catch (_) {
throw ProcessExit(ExitCode.software.code);
}

return artifactFile;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export 'get_apks_command.dart';
export 'releases_command.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'package:shorebird_cli/src/commands/releases/releases.dart';
import 'package:shorebird_cli/src/shorebird_command.dart';

/// {@template releases_command}
/// Commands for managing Shorebird releases.
/// {@endtemplate}
class ReleasesCommand extends ShorebirdCommand {
/// {@macro releases_command}
ReleasesCommand() {
addSubcommand(GetApksCommand());
}

@override
String get name => 'releases';

@override
String get description => 'Manage Shorebird releases';
}
16 changes: 16 additions & 0 deletions packages/shorebird_cli/lib/src/common_arguments.dart
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ Entries from "--dart-define" with identical keys take precedence over entries fr
'''Export an IPA with these options. See "xcodebuild -h" for available exportOptionsPlist keys (iOS only).''',
);

/// An argument that allows the user to specify a build flavor. You will most
/// likely want to provide a custom description for this argument that more
/// thoroughly explains what the flavor is used for.
static const flavorArg = ArgumentDescriber(
name: 'flavor',
description: 'The app flavor',
);

/// An argument that allows the user to specify a public key file that will be
/// used to validate patch signatures.
static const publicKeyArg = ArgumentDescriber(
Expand All @@ -102,6 +110,14 @@ The path for a private key .pem file that will be used to sign the patch artifac
''',
);

/// An argument that allows the user to specify a release version. You will
/// most likely want to provide a custom description for this argument that
/// more thoroughly explains what the release version is used for.
static const releaseVersionArg = ArgumentDescriber(
name: 'release-version',
description: 'The version of the release (e.g. "1.0.0").',
);

/// An argument that allows the user to specify a directory where program
/// symbols are stored.
static const splitDebugInfoArg = ArgumentDescriber(
Expand Down
3 changes: 2 additions & 1 deletion packages/shorebird_cli/lib/src/executables/bundletool.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,15 @@ class Bundletool {
Future<void> buildApks({
required String bundle,
required String output,
bool universal = true,
}) async {
final result = await _exec(
[
'build-apks',
'--overwrite',
'--bundle=$bundle',
'--output=$output',
'--mode=universal',
if (universal) '--mode=universal',
],
);
if (result.exitCode != 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ class ShorebirdCliCommandRunner extends CompletionCommandRunner<int> {
addCommand(PatchesCommand());
addCommand(PreviewCommand());
addCommand(ReleaseCommand());
addCommand(ReleasesCommand());
addCommand(RunCommand());
addCommand(UpgradeCommand());
}
Expand Down
Loading

0 comments on commit 1483151

Please sign in to comment.