Skip to content

Commit

Permalink
[Android Editor] Enable apk export
Browse files Browse the repository at this point in the history
- Enable apk signing (and verification) using the apksig library from https://android.googlesource.com/platform/tools/apksig/+/ac5cbb07d87cc342fcf07715857a812305d69888
- Update the export logic to enable apk generation and signing from the Android editor

Note: Only legacy builds are supported. Gradle builds are not supported at this point in time.
  • Loading branch information
m4gr3d committed Aug 17, 2024
1 parent 3bd0784 commit 2dcc4b9
Show file tree
Hide file tree
Showing 145 changed files with 31,326 additions and 70 deletions.
3 changes: 2 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ repos:
modules/gdscript/tests/scripts/parser/features/mixed_indentation_on_blank_lines\.gd$|
modules/gdscript/tests/scripts/parser/warnings/empty_file_newline_comment\.notest\.gd$|
modules/gdscript/tests/scripts/parser/warnings/empty_file_newline\.notest\.gd$|
platform/android/java/lib/src/com/google/.*
platform/android/java/lib/src/com/google/.*|
platform/android/java/lib/src/com/android/.*
)
- id: dotnet-format
Expand Down
1 change: 1 addition & 0 deletions COPYRIGHT.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ License: Expat

Files: ./platform/android/java/lib/aidl/com/android/*
./platform/android/java/lib/res/layout/status_bar_ongoing_event_progress_bar.xml
./platform/android/java/lib/src/com/android/*
./platform/android/java/lib/src/com/google/android/*
./platform/android/java/lib/src/org/godotengine/godot/input/InputManagerCompat.java
./platform/android/java/lib/src/org/godotengine/godot/input/InputManagerV16.java
Expand Down
2 changes: 0 additions & 2 deletions editor/editor_node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7225,9 +7225,7 @@ EditorNode::EditorNode() {
#endif

settings_menu->add_item(TTR("Manage Editor Features..."), SETTINGS_MANAGE_FEATURE_PROFILES);
#ifndef ANDROID_ENABLED
settings_menu->add_item(TTR("Manage Export Templates..."), SETTINGS_MANAGE_EXPORT_TEMPLATES);
#endif
#if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED)
settings_menu->add_item(TTR("Configure FBX Importer..."), SETTINGS_MANAGE_FBX_IMPORTER);
#endif
Expand Down
4 changes: 4 additions & 0 deletions editor/editor_paths.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,11 @@ String EditorPaths::get_export_templates_dir() const {
}

String EditorPaths::get_debug_keystore_path() const {
#ifdef ANDROID_ENABLED
return "assets://keystores/debug.keystore";
#else
return get_data_dir().path_join("keystores/debug.keystore");
#endif
}

String EditorPaths::get_project_settings_dir() const {
Expand Down
2 changes: 0 additions & 2 deletions editor/export/editor_export_platform.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1861,7 +1861,6 @@ void EditorExportPlatform::gen_export_flags(Vector<String> &r_flags, int p_flags
bool EditorExportPlatform::can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const {
bool valid = true;

#ifndef ANDROID_ENABLED
String templates_error;
valid = valid && has_valid_export_configuration(p_preset, templates_error, r_missing_templates, p_debug);

Expand All @@ -1886,7 +1885,6 @@ bool EditorExportPlatform::can_export(const Ref<EditorExportPreset> &p_preset, S
if (!export_plugins_warning.is_empty()) {
r_error += export_plugins_warning;
}
#endif

String project_configuration_error;
valid = valid && has_valid_project_configuration(p_preset, project_configuration_error);
Expand Down
6 changes: 5 additions & 1 deletion editor/export/export_template_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@ void ExportTemplateManager::_update_template_status() {
TreeItem *ti = installed_table->create_item(installed_root);
ti->set_text(0, version_string);

#ifndef ANDROID_ENABLED
ti->add_button(0, get_editor_theme_icon(SNAME("Folder")), OPEN_TEMPLATE_FOLDER, false, TTR("Open the folder containing these templates."));
#endif
ti->add_button(0, get_editor_theme_icon(SNAME("Remove")), UNINSTALL_TEMPLATE, false, TTR("Uninstall these templates."));
}
}
Expand Down Expand Up @@ -924,11 +926,13 @@ ExportTemplateManager::ExportTemplateManager() {
current_installed_path->set_h_size_flags(Control::SIZE_EXPAND_FILL);
current_installed_hb->add_child(current_installed_path);

current_open_button = memnew(Button);
#ifndef ANDROID_ENABLED
Button *current_open_button = memnew(Button);
current_open_button->set_text(TTR("Open Folder"));
current_open_button->set_tooltip_text(TTR("Open the folder containing installed templates for the current version."));
current_installed_hb->add_child(current_open_button);
current_open_button->connect(SceneStringName(pressed), callable_mp(this, &ExportTemplateManager::_open_template_folder).bind(VERSION_FULL_CONFIG));
#endif

current_uninstall_button = memnew(Button);
current_uninstall_button->set_text(TTR("Uninstall"));
Expand Down
1 change: 0 additions & 1 deletion editor/export/export_template_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ class ExportTemplateManager : public AcceptDialog {

HBoxContainer *current_installed_hb = nullptr;
LineEdit *current_installed_path = nullptr;
Button *current_open_button = nullptr;
Button *current_uninstall_button = nullptr;

VBoxContainer *install_options_vb = nullptr;
Expand Down
14 changes: 1 addition & 13 deletions editor/export/project_export.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1147,10 +1147,8 @@ void ProjectExportDialog::_export_project_to_path(const String &p_path) {
}

void ProjectExportDialog::_export_all_dialog() {
#ifndef ANDROID_ENABLED
export_all_dialog->show();
export_all_dialog->popup_centered(Size2(300, 80));
#endif
}

void ProjectExportDialog::_export_all_dialog_action(const String &p_str) {
Expand Down Expand Up @@ -1491,13 +1489,9 @@ ProjectExportDialog::ProjectExportDialog() {
set_ok_button_text(TTR("Export PCK/ZIP..."));
get_ok_button()->set_tooltip_text(TTR("Export the project resources as a PCK or ZIP package. This is not a playable build, only the project data without a Godot executable."));
get_ok_button()->set_disabled(true);
#ifdef ANDROID_ENABLED
export_button = memnew(Button);
export_button->hide();
#else

export_button = add_button(TTR("Export Project..."), !DisplayServer::get_singleton()->get_swap_cancel_ok(), "export");
export_button->set_tooltip_text(TTR("Export the project as a playable build (Godot executable and project data) for the selected preset."));
#endif
export_button->connect(SceneStringName(pressed), callable_mp(this, &ProjectExportDialog::_export_project));
// Disable initially before we select a valid preset
export_button->set_disabled(true);
Expand All @@ -1510,14 +1504,8 @@ ProjectExportDialog::ProjectExportDialog() {
export_all_dialog->add_button(TTR("Debug"), true, "debug");
export_all_dialog->add_button(TTR("Release"), true, "release");
export_all_dialog->connect("custom_action", callable_mp(this, &ProjectExportDialog::_export_all_dialog_action));
#ifdef ANDROID_ENABLED
export_all_dialog->hide();

export_all_button = memnew(Button);
export_all_button->hide();
#else
export_all_button = add_button(TTR("Export All..."), !DisplayServer::get_singleton()->get_swap_cancel_ok(), "export");
#endif
export_all_button->connect(SceneStringName(pressed), callable_mp(this, &ProjectExportDialog::_export_all_dialog));
export_all_button->set_disabled(true);

Expand Down
10 changes: 7 additions & 3 deletions platform/android/dir_access_jandroid.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ bool DirAccessJAndroid::dir_exists(String p_dir) {
}
}

Error DirAccessJAndroid::make_dir_recursive(const String &p_dir) {
Error DirAccessJAndroid::make_dir(String p_dir) {
// Check if the directory exists already
if (dir_exists(p_dir)) {
return ERR_ALREADY_EXISTS;
Expand All @@ -242,8 +242,12 @@ Error DirAccessJAndroid::make_dir_recursive(const String &p_dir) {
}
}

Error DirAccessJAndroid::make_dir(String p_dir) {
return make_dir_recursive(p_dir);
Error DirAccessJAndroid::make_dir_recursive(const String &p_dir) {
Error err = make_dir(p_dir);
if (err != OK && err != ERR_ALREADY_EXISTS) {
ERR_FAIL_V_MSG(err, "Could not create directory: " + p_dir);
}
return OK;
}

Error DirAccessJAndroid::rename(String p_from, String p_to) {
Expand Down
2 changes: 1 addition & 1 deletion platform/android/dir_access_jandroid.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ class DirAccessJAndroid : public DirAccessUnix {

virtual bool is_link(String p_file) override { return false; }
virtual String read_link(String p_file) override { return p_file; }
virtual Error create_link(String p_source, String p_target) override { return FAILED; }
virtual Error create_link(String p_source, String p_target) override { return ERR_UNAVAILABLE; }

virtual uint64_t get_space_left() override;

Expand Down
11 changes: 6 additions & 5 deletions platform/android/export/export.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,17 @@ void register_android_exporter_types() {
}

void register_android_exporter() {
#ifndef ANDROID_ENABLED
EDITOR_DEF("export/android/java_sdk_path", OS::get_singleton()->get_environment("JAVA_HOME"));
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/java_sdk_path", PROPERTY_HINT_GLOBAL_DIR));
EDITOR_DEF("export/android/android_sdk_path", OS::get_singleton()->get_environment("ANDROID_HOME"));
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/android_sdk_path", PROPERTY_HINT_GLOBAL_DIR));
EDITOR_DEF("export/android/debug_keystore", EditorPaths::get_singleton()->get_debug_keystore_path());
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/debug_keystore", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks"));
EDITOR_DEF("export/android/debug_keystore_user", DEFAULT_ANDROID_KEYSTORE_DEBUG_USER);
EDITOR_DEF("export/android/debug_keystore_pass", DEFAULT_ANDROID_KEYSTORE_DEBUG_PASSWORD);
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/debug_keystore_pass", PROPERTY_HINT_PASSWORD));

#ifndef ANDROID_ENABLED
EDITOR_DEF("export/android/java_sdk_path", OS::get_singleton()->get_environment("JAVA_HOME"));
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/java_sdk_path", PROPERTY_HINT_GLOBAL_DIR));
EDITOR_DEF("export/android/android_sdk_path", OS::get_singleton()->get_environment("ANDROID_HOME"));
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/android_sdk_path", PROPERTY_HINT_GLOBAL_DIR));
EDITOR_DEF("export/android/force_system_user", false);

EDITOR_DEF("export/android/shutdown_adb_on_exit", true);
Expand Down
92 changes: 64 additions & 28 deletions platform/android/export/export_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@
#include "modules/svg/image_loader_svg.h"
#endif

#ifdef ANDROID_ENABLED
#include "../os_android.h"
#endif

#include <string.h>

static const char *android_perms[] = {
Expand Down Expand Up @@ -2417,6 +2421,10 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
err += template_err;
}
} else {
#ifdef ANDROID_ENABLED
err += TTR("Gradle build is not supported for the Android editor.") + "\n";
valid = false;
#else
// Validate the custom gradle android source template.
bool android_source_template_valid = false;
const String android_source_template = p_preset->get("gradle_build/android_source_template");
Expand All @@ -2439,6 +2447,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
}

valid = installed_android_build_template && !r_missing_templates;
#endif
}

// Validate the rest of the export configuration.
Expand Down Expand Up @@ -2475,6 +2484,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
err += TTR("Release keystore incorrectly configured in the export preset.") + "\n";
}

#ifndef ANDROID_ENABLED
String java_sdk_path = EDITOR_GET("export/android/java_sdk_path");
if (java_sdk_path.is_empty()) {
err += TTR("A valid Java SDK path is required in Editor Settings.") + "\n";
Expand Down Expand Up @@ -2547,6 +2557,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
valid = false;
}
}
#endif

if (!err.is_empty()) {
r_error = err;
Expand Down Expand Up @@ -2717,23 +2728,9 @@ void EditorExportPlatformAndroid::get_command_line_flags(const Ref<EditorExportP

Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &export_path, EditorProgress &ep) {
int export_format = int(p_preset->get("gradle_build/export_format"));
String export_label = export_format == EXPORT_FORMAT_AAB ? "AAB" : "APK";
String release_keystore = _get_keystore_path(p_preset, false);
String release_username = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER);
String release_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS);
String target_sdk_version = p_preset->get("gradle_build/target_sdk");
if (!target_sdk_version.is_valid_int()) {
target_sdk_version = itos(DEFAULT_TARGET_SDK_VERSION);
}
String apksigner = get_apksigner_path(target_sdk_version.to_int(), true);
print_verbose("Starting signing of the " + export_label + " binary using " + apksigner);
if (apksigner == "<FAILED>") {
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("All 'apksigner' tools located in Android SDK 'build-tools' directory failed to execute. Please check that you have the correct version installed for your target sdk version. The resulting %s is unsigned."), export_label));
return OK;
}
if (!FileAccess::exists(apksigner)) {
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("'apksigner' could not be found. Please check that the command is available in the Android SDK build-tools directory. The resulting %s is unsigned."), export_label));
return OK;
if (export_format == EXPORT_FORMAT_AAB) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("AAB signing is not supported"));
return FAILED;
}

String keystore;
Expand All @@ -2750,15 +2747,15 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre
user = EDITOR_GET("export/android/debug_keystore_user");
}

if (ep.step(vformat(TTR("Signing debug %s..."), export_label), 104)) {
if (ep.step(TTR("Signing debug APK..."), 104)) {
return ERR_SKIP;
}
} else {
keystore = release_keystore;
password = release_password;
user = release_username;
keystore = _get_keystore_path(p_preset, false);
password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS);
user = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER);

if (ep.step(vformat(TTR("Signing release %s..."), export_label), 104)) {
if (ep.step(TTR("Signing release APK..."), 104)) {
return ERR_SKIP;
}
}
Expand All @@ -2768,6 +2765,36 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre
return ERR_FILE_CANT_OPEN;
}

String apk_path = export_path;
if (apk_path.is_relative_path()) {
apk_path = OS::get_singleton()->get_resource_dir().path_join(apk_path);
}
apk_path = ProjectSettings::get_singleton()->globalize_path(apk_path).simplify_path();

Error err;
#ifdef ANDROID_ENABLED
err = OS_Android::get_singleton()->sign_apk(apk_path, apk_path, keystore, user, password);
if (err != OK) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Unable to sign apk."));
return err;
}
#else
String target_sdk_version = p_preset->get("gradle_build/target_sdk");
if (!target_sdk_version.is_valid_int()) {
target_sdk_version = itos(DEFAULT_TARGET_SDK_VERSION);
}

String apksigner = get_apksigner_path(target_sdk_version.to_int(), true);
print_verbose("Starting signing of the APK binary using " + apksigner);
if (apksigner == "<FAILED>") {
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("All 'apksigner' tools located in Android SDK 'build-tools' directory failed to execute. Please check that you have the correct version installed for your target sdk version. The resulting APK is unsigned."));
return OK;
}
if (!FileAccess::exists(apksigner)) {
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("'apksigner' could not be found. Please check that the command is available in the Android SDK build-tools directory. The resulting APK is unsigned."));
return OK;
}

String output;
List<String> args;
args.push_back("sign");
Expand All @@ -2778,7 +2805,7 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre
args.push_back("pass:" + password);
args.push_back("--ks-key-alias");
args.push_back(user);
args.push_back(export_path);
args.push_back(apk_path);
if (OS::get_singleton()->is_stdout_verbose() && p_debug) {
// We only print verbose logs with credentials for debug builds to avoid leaking release keystore credentials.
print_verbose("Signing debug binary using: " + String("\n") + apksigner + " " + join_list(args, String(" ")));
Expand All @@ -2790,7 +2817,7 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre
print_line("Signing binary using: " + String("\n") + apksigner + " " + join_list(redacted_args, String(" ")));
}
int retval;
Error err = OS::get_singleton()->execute(apksigner, args, &output, &retval, true);
err = OS::get_singleton()->execute(apksigner, args, &output, &retval, true);
if (err != OK) {
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start apksigner executable."));
return err;
Expand All @@ -2802,15 +2829,23 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("output: \n%s"), output));
return ERR_CANT_CREATE;
}
#endif

if (ep.step(vformat(TTR("Verifying %s..."), export_label), 105)) {
if (ep.step(TTR("Verifying APK..."), 105)) {
return ERR_SKIP;
}

#ifdef ANDROID_ENABLED
err = OS_Android::get_singleton()->verify_apk(apk_path);
if (err != OK) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Unable to verify signed apk."));
return err;
}
#else
args.clear();
args.push_back("verify");
args.push_back("--verbose");
args.push_back(export_path);
args.push_back(apk_path);
if (p_debug) {
print_verbose("Verifying signed build using: " + String("\n") + apksigner + " " + join_list(args, String(" ")));
}
Expand All @@ -2823,10 +2858,11 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre
}
print_verbose(output);
if (retval) {
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("'apksigner' verification of %s failed."), export_label));
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("'apksigner' verification of APK failed."));
add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("output: \n%s"), output));
return ERR_CANT_CREATE;
}
#endif

print_verbose("Successfully completed signing build.");
return OK;
Expand Down Expand Up @@ -3320,7 +3356,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
src_apk = find_export_template("android_release.apk");
}
if (src_apk.is_empty()) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Package not found: \"%s\"."), src_apk));
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("%s export template not found: \"%s\"."), (p_debug ? "Debug" : "Release"), src_apk));
return ERR_FILE_NOT_FOUND;
}
}
Expand Down
2 changes: 1 addition & 1 deletion platform/android/file_access_android.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class FileAccessAndroid : public FileAccess {

virtual uint64_t _get_modified_time(const String &p_file) override { return 0; }
virtual BitField<FileAccess::UnixPermissionFlags> _get_unix_permissions(const String &p_file) override { return 0; }
virtual Error _set_unix_permissions(const String &p_file, BitField<FileAccess::UnixPermissionFlags> p_permissions) override { return FAILED; }
virtual Error _set_unix_permissions(const String &p_file, BitField<FileAccess::UnixPermissionFlags> p_permissions) override { return ERR_UNAVAILABLE; }

virtual bool _get_hidden_attribute(const String &p_file) override { return false; }
virtual Error _set_hidden_attribute(const String &p_file, bool p_hidden) override { return ERR_UNAVAILABLE; }
Expand Down
Loading

0 comments on commit 2dcc4b9

Please sign in to comment.