1
0
mirror of https://github.com/godotengine/godot.git synced 2025-12-02 16:48:55 +00:00

Enable Gradle builds on the Android editor via a dedicated build app

Co-authored-by: Logan Lang <devloglogan@gmail.com>
This commit is contained in:
David Snopek
2025-03-13 08:04:15 -05:00
parent 3a97723ff2
commit 5593a0b2b2
16 changed files with 928 additions and 19 deletions

View File

@@ -8528,8 +8528,8 @@ EditorNode::EditorNode() {
project_menu->add_separator();
project_menu->add_shortcut(ED_SHORTCUT_AND_COMMAND("editor/export", TTRC("Export..."), Key::NONE, TTRC("Export")), PROJECT_EXPORT);
project_menu->add_item(TTRC("Pack Project as ZIP..."), PROJECT_PACK_AS_ZIP);
#ifndef ANDROID_ENABLED
project_menu->add_item(TTRC("Install Android Build Template..."), PROJECT_INSTALL_ANDROID_SOURCE);
#ifndef ANDROID_ENABLED
project_menu->add_item(TTRC("Open User Data Folder"), PROJECT_OPEN_USER_DATA_FOLDER);
#endif

View File

@@ -0,0 +1,192 @@
/**************************************************************************/
/* android_editor_gradle_runner.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#ifdef ANDROID_ENABLED
#include "android_editor_gradle_runner.h"
#include "editor/editor_interface.h"
#include "scene/gui/dialogs.h"
#include "scene/gui/rich_text_label.h"
#include "../java_godot_wrapper.h"
#include "../os_android.h"
void AndroidEditorGradleRunner::run_gradle(const String &p_project_path, const String &p_build_path, const List<String> &p_gradle_build_args, const List<String> &p_gradle_copy_args) {
project_path = p_project_path;
build_path = p_build_path;
gradle_build_args = p_gradle_build_args;
gradle_copy_args = p_gradle_copy_args;
if (output_dialog == nullptr) {
output_label = memnew(RichTextLabel);
output_label->set_selection_enabled(true);
output_label->set_context_menu_enabled(true);
output_label->set_scroll_follow(true);
output_dialog = memnew(ConfirmationDialog);
output_dialog->set_unparent_when_invisible(true);
output_dialog->set_title(TTR("Building Android Project (gradle)"));
output_dialog->add_child(output_label);
output_dialog->connect("canceled", callable_mp(this, &AndroidEditorGradleRunner::_android_gradle_build_cancel));
}
output_label->clear();
output_dialog->get_ok_button()->set_disabled(true);
EditorInterface::get_singleton()->popup_dialog_centered_ratio(output_dialog);
state = STATE_BUILDING;
_android_gradle_build_connect();
}
void AndroidEditorGradleRunner::_android_gradle_build_connect() {
_android_gradle_build_output(0, TTR("> Connecting to Gradle Build Environment..."));
GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
if (!godot_java->build_env_connect(callable_mp(this, &AndroidEditorGradleRunner::_android_gradle_build_build))) {
_android_gradle_build_failed(TTR("Unable to connect to Gradle Build Environment service"));
}
}
void AndroidEditorGradleRunner::_android_gradle_build_disconnect() {
GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
godot_java->build_env_disconnect();
}
void AndroidEditorGradleRunner::_android_gradle_build_output(int p_type, const String &p_line) {
if (p_type == 0) {
print_line(p_line);
output_label->append_text("[color=green]" + p_line + "[/color]\n");
} else if (p_type == 1) {
print_line(p_line);
output_label->add_text(p_line + "\n");
} else {
print_error(p_line);
output_label->append_text("[color=red]" + p_line + "[/color]\n");
}
}
void AndroidEditorGradleRunner::_android_gradle_build_build() {
_android_gradle_build_output(0, TTR("> Starting Gradle build..."));
GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
job_id = godot_java->build_env_execute(
"gradle",
gradle_build_args,
project_path,
build_path,
callable_mp(this, &AndroidEditorGradleRunner::_android_gradle_build_output),
callable_mp(this, &AndroidEditorGradleRunner::_android_gradle_build_build_callback));
if (job_id < 0) {
_android_gradle_build_failed(TTR("Failed to execute Gradle command"));
}
}
void AndroidEditorGradleRunner::_android_gradle_build_build_callback(int p_exit_code) {
job_id = -1;
if (p_exit_code != 0) {
_android_gradle_build_failed();
return;
}
_android_gradle_build_copy();
}
void AndroidEditorGradleRunner::_android_gradle_build_copy() {
_android_gradle_build_output(0, TTR("> Copying Gradle artifacts..."));
GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
job_id = godot_java->build_env_execute(
"gradle",
gradle_copy_args,
project_path,
build_path,
callable_mp(this, &AndroidEditorGradleRunner::_android_gradle_build_output),
callable_mp(this, &AndroidEditorGradleRunner::_android_gradle_build_copy_callback));
if (job_id < 0) {
_android_gradle_build_failed(TTR("Failed to execute Gradle command"));
}
}
void AndroidEditorGradleRunner::_android_gradle_build_copy_callback(int p_exit_code) {
job_id = -1;
if (p_exit_code != 0) {
_android_gradle_build_failed();
} else {
_android_gradle_build_clean_project(true);
}
}
void AndroidEditorGradleRunner::_android_gradle_build_clean_project(bool p_was_successful) {
if (state != STATE_CLEANING) {
state = STATE_CLEANING;
if (p_was_successful) {
output_dialog->hide();
} else {
output_dialog->get_ok_button()->set_disabled(false);
}
GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
godot_java->build_env_clean_project(
project_path,
build_path,
callable_mp(this, &AndroidEditorGradleRunner::_android_gradle_build_clean_project_callback));
}
}
void AndroidEditorGradleRunner::_android_gradle_build_clean_project_callback() {
// Ensure we haven't switched back to STATE_BUILDING in the meantime.
if (state == STATE_CLEANING) {
_android_gradle_build_disconnect();
state = STATE_IDLE;
}
}
void AndroidEditorGradleRunner::_android_gradle_build_failed(const String &p_msg) {
job_id = -1;
if (p_msg != "") {
_android_gradle_build_output(1, p_msg);
}
_android_gradle_build_clean_project(false);
}
void AndroidEditorGradleRunner::_android_gradle_build_cancel() {
if (job_id > 0) {
GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
godot_java->build_env_cancel(job_id);
_android_gradle_build_clean_project(false);
}
}
#endif // ANDROID_ENABLED

View File

@@ -0,0 +1,76 @@
/**************************************************************************/
/* android_editor_gradle_runner.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#ifdef ANDROID_ENABLED
#include "core/object/object.h"
class ConfirmationDialog;
class RichTextLabel;
class AndroidEditorGradleRunner : public Object {
GDCLASS(AndroidEditorGradleRunner, Object);
RichTextLabel *output_label = nullptr;
ConfirmationDialog *output_dialog = nullptr;
enum State {
STATE_IDLE,
STATE_BUILDING,
STATE_CLEANING,
};
State state = STATE_IDLE;
String project_path;
String build_path;
List<String> gradle_build_args;
List<String> gradle_copy_args;
int64_t job_id;
void _android_gradle_build_connect();
void _android_gradle_build_disconnect();
void _android_gradle_build_output(int p_type, const String &p_line);
void _android_gradle_build_build();
void _android_gradle_build_build_callback(int p_exit_code);
void _android_gradle_build_copy();
void _android_gradle_build_copy_callback(int p_exit_code);
void _android_gradle_build_clean_project(bool p_was_successful);
void _android_gradle_build_clean_project_callback();
void _android_gradle_build_failed(const String &p_msg = String());
void _android_gradle_build_cancel();
public:
void run_gradle(const String &p_project_path, const String &p_build_path, const List<String> &p_gradle_build_args, const List<String> &p_gradle_copy_args);
};
#endif // ANDROID_ENABLED

View File

@@ -60,7 +60,9 @@
#endif
#ifdef ANDROID_ENABLED
#include "../java_godot_wrapper.h"
#include "../os_android.h"
#include "android_editor_gradle_runner.h"
#endif
static const char *ANDROID_PERMS[] = {
@@ -2015,6 +2017,11 @@ String EditorExportPlatformAndroid::get_export_option_warning(const EditorExport
if (!enabled_deprecated_plugins_names.is_empty() && !gradle_build_enabled) {
return TTR("\"Use Gradle Build\" must be enabled to use the plugins.");
}
#ifdef ANDROID_ENABLED
if (gradle_build_enabled) {
return TTR("Support for \"Use Gradle Build\" on Android is currently experimental.");
}
#endif // ANDROID_ENABLED
} else if (p_name == "gradle_build/compress_native_libraries") {
bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");
if (bool(p_preset->get("gradle_build/compress_native_libraries")) && !gradle_build_enabled) {
@@ -2100,7 +2107,7 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "gradle_build/use_gradle_build"), false, true, true));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "gradle_build/use_gradle_build"), false, true, false));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/gradle_build_directory", PROPERTY_HINT_PLACEHOLDER_TEXT, "res://android"), "", false, false));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/android_source_template", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "gradle_build/compress_native_libraries"), false, false, true));
@@ -2889,10 +2896,6 @@ 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");
@@ -2915,7 +2918,6 @@ 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.
@@ -3665,6 +3667,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
}
}
const String assets_directory = get_assets_directory(p_preset, export_format);
#ifndef ANDROID_ENABLED
String java_sdk_path = EDITOR_GET("export/android/java_sdk_path");
if (java_sdk_path.is_empty()) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Java SDK path must be configured in Editor Settings at 'export/android/java_sdk_path'."));
@@ -3678,6 +3681,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
return ERR_UNCONFIGURED;
}
print_verbose("Android sdk path: " + sdk_path);
#endif
// TODO: should we use "package/name" or "application/config/name"?
String project_name = get_project_name(p_preset, p_preset->get("package/name"));
@@ -3738,14 +3742,17 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
return err;
}
}
print_verbose("Storing command line flags...");
store_file_at_path(assets_directory + "/_cl_", command_line_flags);
#ifndef ANDROID_ENABLED
print_verbose("Updating JAVA_HOME environment to " + java_sdk_path);
OS::get_singleton()->set_environment("JAVA_HOME", java_sdk_path);
print_verbose("Updating ANDROID_HOME environment to " + sdk_path);
OS::get_singleton()->set_environment("ANDROID_HOME", sdk_path);
#endif
String build_command;
#ifdef WINDOWS_ENABLED
@@ -3832,8 +3839,10 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
String addons_directory = ProjectSettings::get_singleton()->globalize_path("res://addons");
#ifndef ANDROID_ENABLED
cmdline.push_back("-p"); // argument to specify the start directory.
cmdline.push_back(build_path); // start directory.
#endif
cmdline.push_back("-Paddons_directory=" + addons_directory); // path to the addon directory as it may contain jar or aar dependencies
cmdline.push_back("-Pexport_package_name=" + package_name); // argument to specify the package name.
cmdline.push_back("-Pexport_version_code=" + version_code); // argument to specify the version code.
@@ -3872,6 +3881,25 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not find debug keystore, unable to export."));
return ERR_FILE_CANT_OPEN;
}
#ifdef ANDROID_ENABLED
if (debug_keystore.begins_with("assets://")) {
// The Gradle build environment app can't access the Godot
// editor's assets, so we need to copy this to temp file.
Error err;
PackedByteArray keystore_data = FileAccess::get_file_as_bytes(debug_keystore, &err);
if (err == OK) {
String temp_dir = build_path + "/.android";
String temp_filename = temp_dir + "/debug.keystore";
DirAccess::make_dir_recursive_absolute(temp_dir);
Ref<FileAccess> temp_file = FileAccess::open(temp_filename, FileAccess::WRITE);
if (temp_file.is_valid()) {
temp_file->store_buffer(keystore_data);
debug_keystore = temp_filename;
}
}
}
#endif
cmdline.push_back("-Pdebug_keystore_file=" + debug_keystore); // argument to specify the debug keystore file.
cmdline.push_back("-Pdebug_keystore_alias=" + debug_user); // argument to specify the debug keystore alias.
@@ -3895,21 +3923,14 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
}
}
String build_project_output;
int result = EditorNode::get_singleton()->execute_and_show_output(TTR("Building Android Project (gradle)"), build_command, cmdline, true, false, &build_project_output);
if (result != 0) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Building of Android project failed, check output for the error:") + "\n\n" + build_project_output);
return ERR_CANT_CREATE;
} else {
print_verbose(build_project_output);
}
List<String> copy_args;
String copy_command = "copyAndRenameBinary";
copy_args.push_back(copy_command);
#ifndef ANDROID_ENABLED
copy_args.push_back("-p"); // argument to specify the start directory.
copy_args.push_back(build_path); // start directory.
#endif
copy_args.push_back("-Pexport_edition=" + edition.to_lower());
@@ -3928,6 +3949,23 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
copy_args.push_back("-Pexport_path=file:" + export_path);
copy_args.push_back("-Pexport_filename=" + export_filename);
#ifdef ANDROID_ENABLED
String project_path = ProjectSettings::get_singleton()->globalize_path("res://");
android_editor_gradle_runner->run_gradle(
project_path,
build_path.substr(project_path.length()),
cmdline,
copy_args);
#else
String build_project_output;
int result = EditorNode::get_singleton()->execute_and_show_output(TTR("Building Android Project (gradle)"), build_command, cmdline, true, false, &build_project_output);
if (result != 0) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Building of Android project failed, check output for the error:") + "\n\n" + build_project_output);
return ERR_CANT_CREATE;
} else {
print_verbose(build_project_output);
}
print_verbose("Copying Android binary using gradle command: " + String("\n") + build_command + " " + join_list(copy_args, String(" ")));
String copy_binary_output;
int copy_result = EditorNode::get_singleton()->execute_and_show_output(TTR("Moving output"), build_command, copy_args, true, false, &copy_binary_output);
@@ -3939,6 +3977,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
}
print_verbose("Successfully completed Android gradle build.");
#endif
return OK;
}
// This is the start of the Legacy build system
@@ -4338,6 +4377,8 @@ void EditorExportPlatformAndroid::initialize() {
_create_editor_debug_keystore_if_needed();
_update_preset_status();
check_for_changes_thread.start(_check_for_changes_poll_thread, this);
#else
android_editor_gradle_runner = memnew(AndroidEditorGradleRunner);
#endif
use_scrcpy = EditorSettings::get_singleton()->get_project_metadata("android", "use_scrcpy", false);
}
@@ -4349,5 +4390,9 @@ EditorExportPlatformAndroid::~EditorExportPlatformAndroid() {
if (check_for_changes_thread.is_started()) {
check_for_changes_thread.wait_to_finish();
}
#else
if (android_editor_gradle_runner) {
memdelete(android_editor_gradle_runner);
}
#endif
}

View File

@@ -60,6 +60,8 @@ struct LauncherIcon {
int dimensions = 0;
};
class AndroidEditorGradleRunner;
class EditorExportPlatformAndroid : public EditorExportPlatform {
GDCLASS(EditorExportPlatformAndroid, EditorExportPlatform);
@@ -106,6 +108,8 @@ class EditorExportPlatformAndroid : public EditorExportPlatform {
static void _check_for_changes_poll_thread(void *ud);
void _update_preset_status();
#else
AndroidEditorGradleRunner *android_editor_gradle_runner = nullptr;
#endif
String get_project_name(const Ref<EditorExportPreset> &p_preset, const String &p_name) const;

View File

@@ -3,6 +3,10 @@
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="auto">
<queries>
<package android:name="org.godotengine.godot_gradle_build_environment" />
</queries>
<supports-screens
android:largeScreens="true"
android:normalScreens="true"

View File

@@ -53,11 +53,12 @@ import androidx.core.content.edit
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.isVisible
import androidx.window.layout.WindowMetricsCalculator
import org.godotengine.editor.buildprovider.GradleBuildProvider
import org.godotengine.editor.embed.EmbeddedGodotGame
import org.godotengine.editor.embed.GameMenuFragment
import org.godotengine.editor.utils.signApk
import org.godotengine.editor.utils.verifyApk
import org.godotengine.godot.BuildConfig
import org.godotengine.godot.BuildProvider
import org.godotengine.godot.Godot
import org.godotengine.godot.GodotActivity
import org.godotengine.godot.GodotLib
@@ -171,6 +172,7 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
}
}
internal val gradleBuildProvider: GradleBuildProvider = GradleBuildProvider(this, this)
internal val editorMessageDispatcher = EditorMessageDispatcher(this)
private val editorLoadingIndicator: View? by lazy { findViewById(R.id.editor_loading_indicator) }
@@ -262,6 +264,11 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
setupGameMenuBar()
}
override fun onDestroy() {
gradleBuildProvider.buildEnvDisconnect()
super.onDestroy()
}
override fun onNewIntent(newIntent: Intent) {
if (newIntent.hasCategory(HYBRID_APP_PANEL_CATEGORY) || newIntent.hasCategory(HYBRID_APP_IMMERSIVE_CATEGORY)) {
val params = retrieveCommandLineParamsFromLaunchIntent(newIntent)
@@ -968,4 +975,8 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
}
override fun isGameEmbeddingSupported() = !isNativeXRDevice(applicationContext)
override fun getBuildProvider(): BuildProvider? {
return gradleBuildProvider
}
}

View File

@@ -0,0 +1,225 @@
/**************************************************************************/
/* GradleBuildEnvironmentClient.kt */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
package org.godotengine.editor.buildprovider
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Bundle
import android.os.Handler
import android.os.IBinder
import android.os.Message
import android.os.Messenger
import android.os.RemoteException
import android.util.Log
import kotlin.collections.set
private const val MSG_EXECUTE_GRADLE = 1
private const val MSG_COMMAND_RESULT = 2
private const val MSG_COMMAND_OUTPUT = 3
private const val MSG_CANCEL_COMMAND = 4
private const val MSG_CLEAN_PROJECT = 5
internal class GradleBuildEnvironmentClient(private val context: Context) {
companion object {
private val TAG = GradleBuildEnvironmentClient::class.java.simpleName
}
private var bound: Boolean = false
private var outgoingMessenger: Messenger? = null
private val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
outgoingMessenger = Messenger(service)
bound = true
Log.i(TAG, "Service connected")
for (callable in connectionCallbacks) {
callable()
}
connectionCallbacks.clear()
connecting = false
}
override fun onServiceDisconnected(name: ComponentName?) {
outgoingMessenger = null
bound = false
Log.i(TAG, "Service disconnected")
}
}
private inner class IncomingHandler: Handler() {
override fun handleMessage(msg: Message) {
when (msg.what) {
MSG_COMMAND_RESULT -> {
this@GradleBuildEnvironmentClient.receiveCommandResult(msg)
}
MSG_COMMAND_OUTPUT -> {
this@GradleBuildEnvironmentClient.receiveCommandOutput(msg)
}
else -> super.handleMessage(msg)
}
}
}
private val incomingMessenger = Messenger(IncomingHandler())
private val connectionCallbacks = mutableListOf<() -> Unit>()
private var connecting = false
private var executionId = 1000
private class ExecutionInfo(val outputCallback: (Int, String) -> Unit, val resultCallback: (Int) -> Unit)
private val executionMap = HashMap<Int, ExecutionInfo>()
fun connect(callback: () -> Unit): Boolean {
if (bound) {
callback()
return true;
}
connectionCallbacks.add(callback)
if (connecting) {
return true;
}
connecting = true;
val intent = Intent("org.godotengine.action.BUILD_PROVIDER").apply {
setPackage("org.godotengine.godot_gradle_build_environment")
}
val info = context.packageManager.resolveService(intent, 0)
if (info == null) {
connecting = false;
Log.e(TAG, "Unable to resolve service")
return false
}
val result = context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
if (!result) {
Log.e(TAG, "Unable to bind to service")
connecting = false;
}
return result;
}
fun disconnect() {
if (bound) {
context.unbindService(connection)
bound = false
}
}
private fun getNextExecutionId(outputCallback: (Int, String) -> Unit, resultCallback: (Int) -> Unit): Int {
val id = executionId++
executionMap[id] = ExecutionInfo(outputCallback, resultCallback)
return id
}
fun execute(arguments: Array<String>, projectPath: String, gradleBuildDir: String, outputCallback: (Int, String) -> Unit, resultCallback: (Int) -> Unit): Int {
if (outgoingMessenger == null) {
return -1
}
val msg: Message = Message.obtain(null, MSG_EXECUTE_GRADLE, getNextExecutionId(outputCallback, resultCallback),0)
msg.replyTo = incomingMessenger
val data = Bundle()
data.putStringArrayList("arguments", ArrayList(arguments.toList()))
data.putString("project_path", projectPath)
data.putString("gradle_build_directory", gradleBuildDir)
msg.data = data
try {
outgoingMessenger?.send(msg)
} catch (e: RemoteException) {
Log.e(TAG, "Unable to execute Gradle command: gradlew ${arguments.joinToString(" ")}", e)
e.printStackTrace()
executionMap.remove(msg.arg1)
resultCallback(255)
return -1
}
return msg.arg1
}
private fun receiveCommandResult(msg: Message) {
val executionInfo = executionMap.remove(msg.arg1)
executionInfo?.resultCallback?.invoke(msg.arg2)
}
private fun receiveCommandOutput(msg: Message) {
val data = msg.data
val line = data.getString("line")
if (line != null) {
val executionInfo = executionMap.get(msg.arg1)
executionInfo?.outputCallback?.invoke(msg.arg2, line)
}
}
fun cancel(jobId: Int) {
if (outgoingMessenger == null) {
return
}
val msg: Message = Message.obtain(null, MSG_CANCEL_COMMAND, jobId, 0)
try {
outgoingMessenger?.send(msg)
} catch (e: RemoteException) {
Log.e(TAG, "Unable to cancel Gradle command: ${jobId}", e)
e.printStackTrace()
}
}
fun cleanProject(projectPath: String, gradleBuildDir: String, resultCallback: (Int) -> Unit) {
if (outgoingMessenger == null) {
return
}
val emptyOutputCallback: (Int, String) -> Unit = { outputType, line -> }
val msg: Message = Message.obtain(null, MSG_CLEAN_PROJECT, getNextExecutionId(emptyOutputCallback, resultCallback), 0)
msg.replyTo = incomingMessenger
val data = Bundle()
data.putString("project_path", projectPath)
data.putString("gradle_build_directory", gradleBuildDir)
msg.data = data
try {
outgoingMessenger?.send(msg)
} catch (e: RemoteException) {
Log.e(TAG, "Unable to clean Gradle project", e)
executionMap.remove(msg.arg1)
resultCallback(0)
e.printStackTrace()
}
}
}

View File

@@ -0,0 +1,95 @@
/**************************************************************************/
/* GradleBuildProvider.kt */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
package org.godotengine.editor.buildprovider
import android.content.Context
import org.godotengine.godot.BuildProvider
import org.godotengine.godot.GodotHost
import org.godotengine.godot.variant.Callable
internal class GradleBuildProvider(
val context: Context,
val host: GodotHost,
) : BuildProvider {
val gradleBuildEnvironmentClient = GradleBuildEnvironmentClient(context)
val godot get() = host.godot
override fun buildEnvConnect(callback: Callable): Boolean {
return gradleBuildEnvironmentClient.connect {
godot?.runOnRenderThread {
callback.call()
}
}
}
override fun buildEnvDisconnect() {
gradleBuildEnvironmentClient.disconnect()
}
override fun buildEnvExecute(
buildTool: String,
arguments: Array<String>,
projectPath: String,
buildDir: String,
outputCallback: Callable,
resultCallback: Callable
): Int {
if (buildTool != "gradle") {
return -1;
}
val outputCb: (Int, String) -> Unit = { outputType, line ->
godot?.runOnRenderThread {
outputCallback.call(outputType, line)
}
}
val resultCb: (Int) -> Unit = { exitCode ->
godot?.runOnRenderThread {
resultCallback.call(exitCode)
}
}
return gradleBuildEnvironmentClient.execute(arguments, projectPath, buildDir, outputCb, resultCb)
}
override fun buildEnvCancel(jobId: Int) {
gradleBuildEnvironmentClient.cancel(jobId)
}
override fun buildEnvCleanProject(projectPath: String, buildDir: String, callback: Callable) {
val cb: (Int) -> Unit = { exitCode ->
godot?.runOnRenderThread {
callback.call()
}
}
gradleBuildEnvironmentClient.cleanProject(projectPath, buildDir, cb)
}
}

View File

@@ -0,0 +1,81 @@
/**************************************************************************/
/* BuildProvider.java */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
package org.godotengine.godot;
import org.godotengine.godot.variant.Callable;
import androidx.annotation.NonNull;
/**
* Provides an environment for executing build commands.
*/
public interface BuildProvider {
/**
* Connects to the build environment.
*
* @param callback The callback to call when connected
* @return Whether or not connecting is possible
*/
boolean buildEnvConnect(@NonNull Callable callback);
/**
* Disconnects from the build environment.
*/
void buildEnvDisconnect();
/**
* Executes a command via the build environment.
*
* @param buildTool The build tool to execute (for example, "gradle")
* @param arguments The argument for the command
* @param projectPath The working directory to use when executing the command
* @param buildDir The build directory within the project
* @param outputCallback The callback to call for each line of output from the command
* @param resultCallback The callback to call when the command is finished running
* @return A positive job id, if successful; otherwise, a negative number
*/
int buildEnvExecute(String buildTool, @NonNull String[] arguments, @NonNull String projectPath, @NonNull String buildDir, @NonNull Callable outputCallback, @NonNull Callable resultCallback);
/**
* Cancels a command executed via the build environment.
*
* @param jobId The job id returned from buildEnvExecute()
*/
void buildEnvCancel(int jobId);
/**
* Requests that a project be cleaned up via the build environment.
*
* @param projectPath The working directory to use when executing the command
* @param buildDir The build directory within the project
*/
void buildEnvCleanProject(@NonNull String projectPath, @NonNull String buildDir, @NonNull Callable callback);
}

View File

@@ -75,6 +75,7 @@ import org.godotengine.godot.utils.benchmarkFile
import org.godotengine.godot.utils.dumpBenchmark
import org.godotengine.godot.utils.endBenchmarkMeasure
import org.godotengine.godot.utils.useBenchmark
import org.godotengine.godot.variant.Callable as GodotCallable
import org.godotengine.godot.xr.XRMode
import java.io.File
import java.io.FileInputStream
@@ -1304,4 +1305,64 @@ class Godot private constructor(val context: Context) {
private fun nativeOnEditorWorkspaceSelected(workspace: String) {
primaryHost?.onEditorWorkspaceSelected(workspace)
}
@Keep
private fun nativeBuildEnvConnect(callback: GodotCallable): Boolean {
try {
val buildProvider = primaryHost?.getBuildProvider()
return buildProvider?.buildEnvConnect(callback) ?: false
} catch (e: Exception) {
Log.e(TAG, "Unable to connect to build environment", e)
return false
}
}
@Keep
private fun nativeBuildEnvDisconnect() {
try {
val buildProvider = primaryHost?.getBuildProvider()
buildProvider?.buildEnvDisconnect()
} catch (e: Exception) {
Log.e(TAG, "Unable to disconnect from build environment", e)
}
}
@Keep
private fun nativeBuildEnvExecute(buildTool: String, arguments: Array<String>, projectPath: String, buildDir: String, outputCallback: GodotCallable, resultCallback: GodotCallable): Int {
try {
val buildProvider = primaryHost?.getBuildProvider()
return buildProvider?.buildEnvExecute(
buildTool,
arguments,
projectPath,
buildDir,
outputCallback,
resultCallback
) ?: -1
} catch (e: Exception) {
Log.e(TAG, "Unable to execute Gradle command in build environment", e);
return -1
}
}
@Keep
private fun nativeBuildEnvCancel(jobId: Int) {
try {
val buildProvider = primaryHost?.getBuildProvider()
buildProvider?.buildEnvCancel(jobId)
} catch (e: Exception) {
Log.e(TAG, "Unable to cancel command in build environment", e)
}
}
@Keep
private fun nativeBuildEnvCleanProject(projectPath: String, buildDir: String, callback: GodotCallable) {
try {
val buildProvider = primaryHost?.getBuildProvider()
buildProvider?.buildEnvCleanProject(projectPath, buildDir, callback)
} catch(e: Exception) {
Log.e(TAG, "Unable to clean project in build environment", e)
}
}
}

View File

@@ -40,7 +40,6 @@ import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.os.Messenger;
import android.text.TextUtils;
@@ -496,4 +495,12 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
parentHost.onEditorWorkspaceSelected(workspace);
}
}
@Override
public BuildProvider getBuildProvider() {
if (parentHost != null) {
return parentHost.getBuildProvider();
}
return null;
}
}

View File

@@ -166,4 +166,13 @@ public interface GodotHost {
activity.runOnUiThread(action);
}
}
/**
* Gets the build provider, if available.
*
* @return the build provider, if available; otherwise, null.
*/
default @Nullable BuildProvider getBuildProvider() {
return null;
}
}

View File

@@ -71,7 +71,7 @@ class Callable private constructor(private val nativeCallablePointer: Long) {
/**
* Calls the method represented by this [Callable]. Arguments can be passed and should match the method's signature.
*/
internal fun call(vararg params: Any): Any? {
fun call(vararg params: Any): Any? {
if (nativeCallablePointer == 0L) {
return null
}

View File

@@ -30,6 +30,8 @@
#include "java_godot_wrapper.h"
#include "jni_utils.h"
// JNIEnv is only valid within the thread it belongs to, in a multi threading environment
// we can't cache it.
// For Godot we call most access methods from our thread and we thus get a valid JNIEnv
@@ -88,6 +90,11 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_godot_instance) {
_set_window_color = p_env->GetMethodID(godot_class, "setWindowColor", "(Ljava/lang/String;)V");
_on_editor_workspace_selected = p_env->GetMethodID(godot_class, "nativeOnEditorWorkspaceSelected", "(Ljava/lang/String;)V");
_get_activity = p_env->GetMethodID(godot_class, "getActivity", "()Landroid/app/Activity;");
_build_env_connect = p_env->GetMethodID(godot_class, "nativeBuildEnvConnect", "(Lorg/godotengine/godot/variant/Callable;)Z");
_build_env_disconnect = p_env->GetMethodID(godot_class, "nativeBuildEnvDisconnect", "()V");
_build_env_execute = p_env->GetMethodID(godot_class, "nativeBuildEnvExecute", "(Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lorg/godotengine/godot/variant/Callable;Lorg/godotengine/godot/variant/Callable;)I");
_build_env_cancel = p_env->GetMethodID(godot_class, "nativeBuildEnvCancel", "(I)V");
_build_env_clean_project = p_env->GetMethodID(godot_class, "nativeBuildEnvCleanProject", "(Ljava/lang/String;Ljava/lang/String;Lorg/godotengine/godot/variant/Callable;)V");
}
GodotJavaWrapper::~GodotJavaWrapper() {
@@ -607,3 +614,84 @@ void GodotJavaWrapper::on_editor_workspace_selected(const String &p_workspace) {
env->CallVoidMethod(godot_instance, _on_editor_workspace_selected, j_workspace);
}
}
bool GodotJavaWrapper::build_env_connect(const Callable &p_callback) {
if (_build_env_connect) {
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL_V(env, false);
jobject j_callback = callable_to_jcallable(env, p_callback);
jboolean result = env->CallBooleanMethod(godot_instance, _build_env_connect, j_callback);
env->DeleteLocalRef(j_callback);
return result;
}
return false;
}
void GodotJavaWrapper::build_env_disconnect() {
if (_build_env_disconnect) {
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL(env);
env->CallVoidMethod(godot_instance, _build_env_disconnect);
}
}
int GodotJavaWrapper::build_env_execute(const String &p_build_tool, const List<String> &p_arguments, const String &p_project_path, const String &p_gradle_build_directory, const Callable &p_output_callback, const Callable &p_result_callback) {
if (_build_env_execute) {
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL_V(env, -1);
jstring j_build_tool = env->NewStringUTF(p_build_tool.utf8().get_data());
jobjectArray j_args = env->NewObjectArray(p_arguments.size(), env->FindClass("java/lang/String"), nullptr);
for (int i = 0; i < p_arguments.size(); i++) {
jstring j_arg = env->NewStringUTF(p_arguments.get(i).utf8().get_data());
env->SetObjectArrayElement(j_args, i, j_arg);
env->DeleteLocalRef(j_arg);
}
jstring j_project_path = env->NewStringUTF(p_project_path.utf8().get_data());
jstring j_gradle_build_directory = env->NewStringUTF(p_gradle_build_directory.utf8().get_data());
jobject j_output_callback = callable_to_jcallable(env, p_output_callback);
jobject j_result_callback = callable_to_jcallable(env, p_result_callback);
jint result = env->CallIntMethod(godot_instance, _build_env_execute, j_build_tool, j_args, j_project_path, j_gradle_build_directory, j_output_callback, j_result_callback);
env->DeleteLocalRef(j_build_tool);
env->DeleteLocalRef(j_args);
env->DeleteLocalRef(j_project_path);
env->DeleteLocalRef(j_gradle_build_directory);
env->DeleteLocalRef(j_output_callback);
env->DeleteLocalRef(j_result_callback);
return result;
}
return -1;
}
void GodotJavaWrapper::build_env_cancel(int p_job_id) {
if (_build_env_cancel) {
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL(env);
env->CallVoidMethod(godot_instance, _build_env_cancel, p_job_id);
}
}
void GodotJavaWrapper::build_env_clean_project(const String &p_project_path, const String &p_gradle_build_directory, const Callable &p_callback) {
if (_build_env_clean_project) {
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL(env);
jstring j_project_path = env->NewStringUTF(p_project_path.utf8().get_data());
jstring j_gradle_build_directory = env->NewStringUTF(p_gradle_build_directory.utf8().get_data());
jobject j_callback = callable_to_jcallable(env, p_callback);
env->CallVoidMethod(godot_instance, _build_env_clean_project, j_project_path, j_gradle_build_directory, j_callback);
env->DeleteLocalRef(j_project_path);
env->DeleteLocalRef(j_gradle_build_directory);
env->DeleteLocalRef(j_callback);
}
}

View File

@@ -84,6 +84,11 @@ private:
jmethodID _set_window_color = nullptr;
jmethodID _on_editor_workspace_selected = nullptr;
jmethodID _get_activity = nullptr;
jmethodID _build_env_connect = nullptr;
jmethodID _build_env_disconnect = nullptr;
jmethodID _build_env_execute = nullptr;
jmethodID _build_env_cancel = nullptr;
jmethodID _build_env_clean_project = nullptr;
public:
GodotJavaWrapper(JNIEnv *p_env, jobject p_godot_instance);
@@ -141,4 +146,10 @@ public:
void set_window_color(const Color &p_color);
void on_editor_workspace_selected(const String &p_workspace);
bool build_env_connect(const Callable &p_callback);
void build_env_disconnect();
int build_env_execute(const String &p_build_tool, const List<String> &p_arguments, const String &p_project_path, const String &p_gradle_build_directory, const Callable &p_output_callback, const Callable &p_result_callback);
void build_env_cancel(int p_job_id);
void build_env_clean_project(const String &p_project_path, const String &p_gradle_build_directory, const Callable &p_callback);
};