Skip to content

Commit

Permalink
feat: Added android create keystore command
Browse files Browse the repository at this point in the history
  • Loading branch information
vanlooverenkoen committed Mar 20, 2024
1 parent 4e163f4 commit 5456209
Show file tree
Hide file tree
Showing 16 changed files with 396 additions and 4 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# 0.14.0

## Feat

- Android create keychain command (cli & plugin)

# 0.13.8

## Fix
Expand Down
Binary file added android/keystore/name.keystore
Binary file not shown.
70 changes: 70 additions & 0 deletions lib/src/core/model/data/android/android_project.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import 'dart:io';

import 'package:impaktfull_cli/impaktfull_cli.dart';
import 'package:impaktfull_cli/src/integrations/android/create_keystore/model/keystore_credentials.dart';
import 'package:path/path.dart';

class AndroidProject {
final Directory directory;

File get gradleFile => File(join('android', 'app', 'build.gradle'));

AndroidProject(this.directory);

void validate() {
_checkIfGradleFileExits();
}

void _checkIfGradleFileExits() {
if (!gradleFile.existsSync()) {
throw ImpaktfullCliError(
'${gradleFile.path} does not exist. Maybe your android project is corrupt');
}
}

void replacePackageName(String newPackageName) {
var content = gradleFile.readAsStringSync();
content = content.replaceAll(
RegExp(r'applicationId "[\w.]*"'), 'applicationId "$newPackageName"');
gradleFile.writeAsStringSync(content);
}

void replaceNamespace(String newNameSpace) {
var content = gradleFile.readAsStringSync();
content = content.replaceAll(
RegExp(r'namespace "[\w.]*"'), 'namespace "$newNameSpace"');
gradleFile.writeAsStringSync(content);
}

void replaceSigningConfig({
required KeyStoreCredentials keyStoreCredentials,
}) {
final name = keyStoreCredentials.name;
var content = gradleFile.readAsStringSync();

final RegExp configBlockPattern = RegExp(
r'.*' +
name +
r' {\n.*storeFile file\("(.*)"\)\n.*storePassword "(.*)"\n.*keyAlias "(.*)"\n.*keyPassword "(.*)"\n.*}',
multiLine: true,
);
final allMatches = configBlockPattern.allMatches(content);
final match = allMatches.first.group(0)!;
final updatedMatch = match
.replaceFirst(RegExp(r'storePassword "(.*?)"'),
'storePassword "${keyStoreCredentials.password}"')
.replaceFirst(RegExp(r'keyAlias "(.*?)"'),
'keyAlias "${keyStoreCredentials.keyAlias}"')
.replaceFirst(RegExp(r'keyPassword "(.*?)"'),
'keyPassword "${keyStoreCredentials.password}"');

final updatedContent = content.replaceAll(configBlockPattern, updatedMatch);

if (updatedContent == content) {
throw ImpaktfullCliError(
'No matching signing config named "$name" found or no changes necessary.');
}

gradleFile.writeAsStringSync(updatedContent);
}
}
3 changes: 3 additions & 0 deletions lib/src/core/model/data/secret.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:impaktfull_cli/src/core/util/logger/logger.dart';
import 'package:uuid/uuid.dart';

class Secret {
String value;
Expand All @@ -7,6 +8,8 @@ class Secret {
ImpaktfullCliLogger.saveSecret(value);
}

static Secret random() => Secret(Uuid().v4());

@override
String toString() => value;
}
33 changes: 31 additions & 2 deletions lib/src/core/util/extensions/arg_result_extensions.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import 'package:args/args.dart';
import 'package:impaktfull_cli/impaktfull_cli.dart';
import 'package:impaktfull_cli/src/core/model/data/secret.dart';
import 'package:impaktfull_cli/src/core/util/extensions/arg_parser_extensions.dart';

extension ArgResultExtensions on ArgResults? {
Expand All @@ -19,7 +18,7 @@ extension ArgResultExtensions on ArgResults? {
}

T? getOption<T>(String option) {
if (T is Secret) {
if (T == Secret) {
final value = this?[option] as String?;
if (value == null) {
return null;
Expand All @@ -32,6 +31,36 @@ extension ArgResultExtensions on ArgResults? {
return value;
}

T? getOptionOrAskInput<T>(String option, String question) {
final value = getOption<T>(option);
if (value != null) return value;
if (T == bool) {
return ImpaktfullCliLogger.askYesNoQuestion(question) as T;
}
final result = ImpaktfullCliLogger.askQuestion(question);
if (result == null) return null;
if (result.isEmpty) return null;
if (T == Secret) {
return Secret(result) as T;
} else if (T == double) {
return double.tryParse(result) as T;
} else if (T == int) {
return int.tryParse(result) as T;
}
return result as T?;
}

T getRequiredOptionOrAskInput<T>(String option, String question) {
T? result;
String? suffix;
do {
result = getOptionOrAskInput<T>(
option, suffix == null ? question : '$question: $suffix');
suffix = '(Required)';
} while (result == null);
return result;
}

T getRequiredOption<T>(String option) {
final value = getOption<T>(option);
if (value == null) throw ArgumentError('$option not found in arguments');
Expand Down
8 changes: 7 additions & 1 deletion lib/src/core/util/logger/logger.dart
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,12 @@ class ImpaktfullCliLogger {
}

// Input
static bool askQuestion(String title) {
static String? askQuestion(String title) {
log('\n$title');
return stdin.readLineSync(encoding: utf8);
}

static bool askYesNoQuestion(String title) {
log('\n$title (y/n)');
bool? result;
do {
Expand Down Expand Up @@ -210,6 +215,7 @@ class ImpaktfullCliLogger {
}

static void endSpinner() {
if (_cliSpinnerActionDescription == null) return;
final message = 'Successfully finished: `$_cliSpinnerActionDescription`';
if (_verbose) {
log('✅ $message');
Expand Down
1 change: 1 addition & 0 deletions lib/src/core/util/runner/runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Future<void> runImpaktfullCli(
await run();
stopwatch.stop();
VerboseLoggingListener.stopListening();
ImpaktfullCliLogger.endSpinner();
ImpaktfullCliLogger.log(
'✅ Success (You just saved ${stopwatch.elapsed.humanReadibleDuration})');
} on ImpaktfullCliExitError catch (e, trace) {
Expand Down
2 changes: 2 additions & 0 deletions lib/src/impaktfull_cli.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:impaktfull_cli/src/core/model/error/impaktfull_cli_error.dart';
import 'package:impaktfull_cli/src/core/plugin/impaktfull_plugin.dart';
import 'package:impaktfull_cli/src/core/util/input_listener/force_quit_listener.dart';
import 'package:impaktfull_cli/src/core/util/input_listener/versbose_logging_listener.dart';
import 'package:impaktfull_cli/src/integrations/android/android_command.dart';
import 'package:impaktfull_cli/src/integrations/appcenter/plugin/appcenter_plugin.dart';
import 'package:impaktfull_cli/src/integrations/apple_certificate/command/apple_certificate_root_command.dart';
import 'package:impaktfull_cli/src/core/util/extensions/arg_parser_extensions.dart';
Expand Down Expand Up @@ -76,6 +77,7 @@ class ImpaktfullCli {
void _initCommands() {
_commands = {
AppleCertificateRootCommand(processRunner: processRunner),
AndroidRootCommand(processRunner: processRunner),
};
}

Expand Down
22 changes: 22 additions & 0 deletions lib/src/integrations/android/android_command.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'package:args/command_runner.dart';
import 'package:impaktfull_cli/src/integrations/android/create_keystore/command/remove/android_create_keystore_command.dart';
import 'package:impaktfull_cli/src/core/command/command/root_command.dart';

class AndroidRootCommand extends RootCommand {
@override
String get name => 'android';

@override
String get description => 'Commands for Android integrations.';

AndroidRootCommand({
required super.processRunner,
});

@override
List<Command<dynamic>> getSubCommands() {
return [
AndroidCreateKeystoreCommand(processRunner: processRunner),
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import 'package:impaktfull_cli/impaktfull_cli.dart';
import 'package:impaktfull_cli/src/core/command/command/cli_command.dart';
import 'package:impaktfull_cli/src/integrations/android/create_keystore/command/remove/android_create_keystore_command_config.dart';
import 'package:impaktfull_cli/src/integrations/android/create_keystore/command/remove/model/android_create_keystore_config_data.dart';
import 'package:impaktfull_cli/src/core/command/config/command_config.dart';
import 'package:impaktfull_cli/src/integrations/android/create_keystore/plugin/android_create_keystore_plugin.dart';

class AndroidCreateKeystoreCommand
extends CliCommand<AndroidCreateKeyStoreConfigData> {
AndroidCreateKeystoreCommand({
required super.processRunner,
});

@override
String get name => 'create_keystore';

@override
String get description => 'Create a keystore';

@override
CommandConfig<AndroidCreateKeyStoreConfigData> getConfig() =>
AndroidCreateKeyStoreCommandConfig();

@override
Future<void> runCommand(AndroidCreateKeyStoreConfigData configData) async {
final androidCreateKeyStorePlugin =
AndroidCreateKeyStorePlugin(processRunner: processRunner);
for (final name in configData.configNames) {
ImpaktfullCliLogger.startSpinner('Creating keystore for $name');
await androidCreateKeyStorePlugin.createKeyStore(
name: name,
dNameFullName: configData.dNameFullName,
dNameOrganization: configData.dNameOrganization,
dNameOrganizationUnit: configData.dNameOrganizationUnit,
dNameCity: configData.dNameCity,
dNameState: configData.dNameState,
dNameCountry: configData.dNameCountry,
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import 'package:args/args.dart';
import 'package:impaktfull_cli/src/integrations/android/create_keystore/command/remove/model/android_create_keystore_config_data.dart';
import 'package:impaktfull_cli/src/core/command/config/command_config.dart';
import 'package:impaktfull_cli/src/core/util/extensions/arg_result_extensions.dart';

class AndroidCreateKeyStoreCommandConfig
extends CommandConfig<AndroidCreateKeyStoreConfigData> {
static const String _optionConfigName = 'configName';
static const String _optionDNameFullName = 'dNameFullName';
static const String _optionDNameOrganization = 'dNameOrganization';
static const String _optionDNameOrganizationUnit = 'dNameOrganizationUnit';
static const String _optionDNameCity = 'dNameCity';
static const String _optionDNameState = 'dNameState';
static const String _optionDNameCountry = 'dNameCountry';
static const defaultSigningConfigNames = ['debug', 'release'];

const AndroidCreateKeyStoreCommandConfig();

@override
void addConfig(ArgParser argParser) {
argParser.addMultiOption(
_optionConfigName,
help: 'Signing config name',
defaultsTo: defaultSigningConfigNames,
);
argParser.addOption(
_optionDNameFullName,
help: 'Certificate info: Your full name',
mandatory: true,
);
argParser.addOption(
_optionDNameOrganization,
help: 'Certificate info: Your organization name',
mandatory: true,
);
argParser.addOption(
_optionDNameOrganizationUnit,
help: 'Certificate info: Your organization unit name',
);
argParser.addOption(
_optionDNameState,
help: 'Certificate info: Your state name',
);
argParser.addOption(
_optionDNameCity,
help: 'Certificate info: Your city name',
);
argParser.addOption(
_optionDNameCountry,
help: 'Certificate info: Your country name',
mandatory: true,
);
}

@override
AndroidCreateKeyStoreConfigData parseResult(ArgResults? argResults) =>
AndroidCreateKeyStoreConfigData(
configNames: argResults.getRequiredOption(_optionConfigName),
dNameFullName: argResults.getRequiredOptionOrAskInput<String>(
_optionDNameFullName, 'Enter your Full Name'),
dNameOrganization: argResults.getRequiredOptionOrAskInput<String>(
_optionDNameOrganization, 'Enter your Organization'),
dNameOrganizationUnit: argResults.getOptionOrAskInput<String>(
_optionDNameOrganizationUnit,
'Enter your Oranization Unit (optional)'),
dNameCity: argResults.getOptionOrAskInput<String>(
_optionDNameCity, 'Enter your City (optional)'),
dNameState: argResults.getOptionOrAskInput(
_optionDNameState, 'Enter your State (optional)'),
dNameCountry: argResults.getRequiredOptionOrAskInput(
_optionDNameCountry, 'Enter the Country'),
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'package:impaktfull_cli/src/core/command/config/command_config.dart';

class AndroidCreateKeyStoreConfigData extends ConfigData {
final List<String> configNames;

final String? dNameFullName;
final String? dNameOrganization;
final String? dNameOrganizationUnit;
final String? dNameCity;
final String? dNameState;
final String? dNameCountry;

const AndroidCreateKeyStoreConfigData({
required this.configNames,
required this.dNameFullName,
required this.dNameOrganization,
required this.dNameOrganizationUnit,
required this.dNameCity,
required this.dNameState,
required this.dNameCountry,
});
}
Loading

0 comments on commit 5456209

Please sign in to comment.