@ -417,6 +417,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform {
name = " noname " ;
pname = pname . replace ( " $genname " , name ) ;
return pname ;
}
@ -1143,11 +1144,12 @@ public:
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : BOOL , " one_click_deploy/clear_previous_install " ) , true ) ) ;
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : STRING , " custom_package/debug " , PROPERTY_HINT_GLOBAL_FILE , " *.apk " ) , " " ) ) ;
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : STRING , " custom_package/release " , PROPERTY_HINT_GLOBAL_FILE , " *.apk " ) , " " ) ) ;
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : BOOL , " custom_package/use_custom_build " ) , false ) ) ;
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : STRING , " command_line/extra_args " ) , " " ) ) ;
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : INT , " version/code " , PROPERTY_HINT_RANGE , " 1,4096,1,or_greater " ) , 1 ) ) ;
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : STRING , " version/name " ) , " 1.0 " ) ) ;
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : STRING , " package/unique_name " , PROPERTY_HINT_PLACEHOLDER_TEXT , " com.example.$gen name" ) , " " ) ) ;
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : STRING , " package/name " , PROPERTY_HINT_PLACEHOLDER_TEXT , " Game Name " ) , " " ) ) ;
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : STRING , " package/unique_name " , PROPERTY_HINT_PLACEHOLDER_TEXT , " ext.domain. name" ) , " org.godotengine.$genname " ) ) ;
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : STRING , " package/name " , PROPERTY_HINT_PLACEHOLDER_TEXT , " Game Name [default if blank] " ) , " " ) ) ;
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : BOOL , " package/signed " ) , true ) ) ;
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : BOOL , " screen/immersive_mode " ) , true ) ) ;
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : INT , " screen/orientation " , PROPERTY_HINT_ENUM , " Landscape,Portrait " ) , 0 ) ) ;
@ -1388,21 +1390,25 @@ public:
virtual bool can_export ( const Ref < EditorExportPreset > & p_preset , String & r_error , bool & r_missing_templates ) const {
String err ;
r_missing_templates = find_export_template ( " android_debug.apk " ) = = String ( ) | | find_export_template ( " android_release.apk " ) = = String ( ) ;
if ( p_preset - > get ( " custom_package/debug " ) ! = " " ) {
if ( FileAccess : : exists ( p_preset - > get ( " custom_package/debug " ) ) ) {
r_missing_templates = false ;
} else {
err + = TTR ( " Custom debug template not found. " ) + " \n " ;
if ( ! bool ( p_preset - > get ( " custom_package/use_custom_build " ) ) ) {
r_missing_templates = find_export_template ( " android_debug.apk " ) = = String ( ) | | find_export_template ( " android_release.apk " ) = = String ( ) ;
if ( p_preset - > get ( " custom_package/debug " ) ! = " " ) {
if ( FileAccess : : exists ( p_preset - > get ( " custom_package/debug " ) ) ) {
r_missing_templates = false ;
} else {
err + = TTR ( " Custom debug template not found. " ) + " \n " ;
}
}
}
if ( p_preset - > get ( " custom_package/release " ) ! = " " ) {
if ( FileAccess : : exists ( p_preset - > get ( " custom_package/release " ) ) ) {
r_missing_templates = false ;
} else {
err + = TTR ( " Custom release template not found. " ) + " \n " ;
if ( p_preset - > get ( " custom_package/release " ) ! = " " ) {
if ( FileAccess : : exists ( p_preset - > get ( " custom_package/release " ) ) ) {
r_missing_templates = false ;
} else {
err + = TTR ( " Custom release template not found. " ) + " \n " ;
}
}
}
@ -1435,6 +1441,30 @@ public:
}
}
if ( bool ( p_preset - > get ( " custom_package/use_custom_build " ) ) ) {
String sdk_path = EditorSettings : : get_singleton ( ) - > get ( " export/android/custom_build_sdk_path " ) ;
if ( sdk_path = = " " ) {
err + = TTR ( " Custom build requires a valid Android SDK path in Editor Settings. " ) + " \n " ;
valid = false ;
} else {
Error errn ;
DirAccess * da = DirAccess : : open ( sdk_path . plus_file ( " tools " ) , & errn ) ;
if ( errn ! = OK ) {
err + = TTR ( " Invalid Android SDK path for custom build in Editor Settings. " ) + " \n " ;
valid = false ;
}
if ( da ) {
memdelete ( da ) ;
}
}
if ( ! FileAccess : : exists ( " res://android/build/build.gradle " ) ) {
err + = TTR ( " Android project is not installed for compiling. Install from Editor menu. " ) + " \n " ;
valid = false ;
}
}
bool apk_expansion = p_preset - > get ( " apk_expansion/enable " ) ;
if ( apk_expansion ) {
@ -1473,6 +1503,260 @@ public:
return list ;
}
void _update_custom_build_project ( ) {
DirAccessRef da = DirAccess : : open ( " res://android " ) ;
ERR_FAIL_COND ( ! da ) ;
Map < String , List < String > > directory_paths ;
Map < String , List < String > > manifest_sections ;
Map < String , List < String > > gradle_sections ;
da - > list_dir_begin ( ) ;
String d = da - > get_next ( ) ;
while ( d ! = String ( ) ) {
if ( ! d . begins_with ( " . " ) & & d ! = " build " & & da - > current_is_dir ( ) ) { //a dir and not the build dir
//add directories found
DirAccessRef ds = DirAccess : : open ( String ( " res://android " ) . plus_file ( d ) ) ;
if ( ds ) {
ds - > list_dir_begin ( ) ;
String sd = ds - > get_next ( ) ;
while ( sd ! = String ( ) ) {
if ( ! sd . begins_with ( " . " ) & & ds - > current_is_dir ( ) ) {
String key = sd . to_upper ( ) ;
if ( ! directory_paths . has ( key ) ) {
directory_paths [ key ] = List < String > ( ) ;
}
String path = ProjectSettings : : get_singleton ( ) - > get_resource_path ( ) . plus_file ( " android " ) . plus_file ( d ) . plus_file ( sd ) ;
directory_paths [ key ] . push_back ( path ) ;
print_line ( " Add: " + sd + " : " + path ) ;
}
sd = ds - > get_next ( ) ;
}
ds - > list_dir_end ( ) ;
}
//parse manifest
{
FileAccessRef f = FileAccess : : open ( String ( " res://android " ) . plus_file ( d ) . plus_file ( " AndroidManifest.conf " ) , FileAccess : : READ ) ;
if ( f ) {
String section ;
while ( ! f - > eof_reached ( ) ) {
String l = f - > get_line ( ) ;
String k = l . strip_edges ( ) ;
if ( k . begins_with ( " [ " ) ) {
section = k . substr ( 1 , k . length ( ) - 2 ) . strip_edges ( ) . to_upper ( ) ;
print_line ( " Section: " + section ) ;
} else if ( k ! = String ( ) ) {
if ( ! manifest_sections . has ( section ) ) {
manifest_sections [ section ] = List < String > ( ) ;
}
manifest_sections [ section ] . push_back ( l ) ;
}
}
f - > close ( ) ;
}
}
//parse gradle
{
FileAccessRef f = FileAccess : : open ( String ( " res://android " ) . plus_file ( d ) . plus_file ( " gradle.conf " ) , FileAccess : : READ ) ;
if ( f ) {
String section ;
while ( ! f - > eof_reached ( ) ) {
String l = f - > get_line ( ) . strip_edges ( ) ;
String k = l . strip_edges ( ) ;
if ( k . begins_with ( " [ " ) ) {
section = k . substr ( 1 , k . length ( ) - 2 ) . strip_edges ( ) . to_upper ( ) ;
print_line ( " Section: " + section ) ;
} else if ( k ! = String ( ) ) {
if ( ! gradle_sections . has ( section ) ) {
gradle_sections [ section ] = List < String > ( ) ;
}
gradle_sections [ section ] . push_back ( l ) ;
}
}
}
}
}
d = da - > get_next ( ) ;
}
da - > list_dir_end ( ) ;
{ //fix gradle build
String new_file ;
{
FileAccessRef f = FileAccess : : open ( " res://android/build/build.gradle " , FileAccess : : READ ) ;
if ( f ) {
while ( ! f - > eof_reached ( ) ) {
String l = f - > get_line ( ) ;
if ( l . begins_with ( " //CHUNK_ " ) ) {
String text = l . replace_first ( " //CHUNK_ " , " " ) ;
int begin_pos = text . find ( " _BEGIN " ) ;
if ( begin_pos ! = - 1 ) {
text = text . substr ( 0 , begin_pos ) ;
text = text . to_upper ( ) ; //just in case
String end_marker = " //CHUNK_ " + text + " _END " ;
size_t pos = f - > get_position ( ) ;
bool found = false ;
while ( ! f - > eof_reached ( ) ) {
l = f - > get_line ( ) ;
if ( l . begins_with ( end_marker ) ) {
found = true ;
break ;
}
}
new_file + = " //CHUNK_ " + text + " _BEGIN \n " ;
if ( ! found ) {
ERR_PRINTS ( " No end marker found in build.gradle for chunk: " + text ) ;
f - > seek ( pos ) ;
} else {
//add chunk lines
if ( gradle_sections . has ( text ) ) {
for ( List < String > : : Element * E = gradle_sections [ text ] . front ( ) ; E ; E = E - > next ( ) ) {
new_file + = E - > get ( ) + " \n " ;
}
}
new_file + = end_marker + " \n " ;
}
} else {
new_file + = l + " \n " ; //pass line by
}
} else if ( l . begins_with ( " //DIR_ " ) ) {
String text = l . replace_first ( " //DIR_ " , " " ) ;
int begin_pos = text . find ( " _BEGIN " ) ;
if ( begin_pos ! = - 1 ) {
text = text . substr ( 0 , begin_pos ) ;
text = text . to_upper ( ) ; //just in case
String end_marker = " //DIR_ " + text + " _END " ;
size_t pos = f - > get_position ( ) ;
bool found = false ;
while ( ! f - > eof_reached ( ) ) {
l = f - > get_line ( ) ;
if ( l . begins_with ( end_marker ) ) {
found = true ;
break ;
}
}
new_file + = " //DIR_ " + text + " _BEGIN \n " ;
if ( ! found ) {
ERR_PRINTS ( " No end marker found in build.gradle for dir: " + text ) ;
f - > seek ( pos ) ;
} else {
//add chunk lines
if ( directory_paths . has ( text ) ) {
for ( List < String > : : Element * E = directory_paths [ text ] . front ( ) ; E ; E = E - > next ( ) ) {
new_file + = " ,' " + E - > get ( ) . replace ( " ' " , " \' " ) + " ' " ;
new_file + = " \n " ;
}
}
new_file + = end_marker + " \n " ;
}
} else {
new_file + = l + " \n " ; //pass line by
}
} else {
new_file + = l + " \n " ;
}
}
}
}
FileAccessRef f = FileAccess : : open ( " res://android/build/build.gradle " , FileAccess : : WRITE ) ;
f - > store_string ( new_file ) ;
f - > close ( ) ;
}
{ //fix manifest
String new_file ;
{
FileAccessRef f = FileAccess : : open ( " res://android/build/AndroidManifest.xml " , FileAccess : : READ ) ;
if ( f ) {
while ( ! f - > eof_reached ( ) ) {
String l = f - > get_line ( ) ;
if ( l . begins_with ( " <!--CHUNK_ " ) ) {
String text = l . replace_first ( " <!--CHUNK_ " , " " ) ;
int begin_pos = text . find ( " _BEGIN--> " ) ;
if ( begin_pos ! = - 1 ) {
text = text . substr ( 0 , begin_pos ) ;
text = text . to_upper ( ) ; //just in case
String end_marker = " <!--CHUNK_ " + text + " _END--> " ;
size_t pos = f - > get_position ( ) ;
bool found = false ;
while ( ! f - > eof_reached ( ) ) {
l = f - > get_line ( ) ;
if ( l . begins_with ( end_marker ) ) {
found = true ;
break ;
}
}
new_file + = " <!--CHUNK_ " + text + " _BEGIN--> \n " ;
if ( ! found ) {
ERR_PRINTS ( " No end marker found in AndroidManifest.conf for chunk: " + text ) ;
f - > seek ( pos ) ;
} else {
//add chunk lines
if ( manifest_sections . has ( text ) ) {
for ( List < String > : : Element * E = manifest_sections [ text ] . front ( ) ; E ; E = E - > next ( ) ) {
new_file + = E - > get ( ) + " \n " ;
}
}
new_file + = end_marker + " \n " ;
}
} else {
new_file + = l + " \n " ; //pass line by
}
} else if ( l . strip_edges ( ) . begins_with ( " <application " ) ) {
String last_tag = " android:icon= \" @drawable/icon \" " ;
int last_tag_pos = l . find ( last_tag ) ;
if ( last_tag_pos = = - 1 ) {
WARN_PRINTS ( " No adding of application tags because could not find last tag for <application: " + last_tag ) ;
new_file + = l + " \n " ;
} else {
String base = l . substr ( 0 , last_tag_pos + last_tag . length ( ) ) ;
if ( manifest_sections . has ( " application_tags " ) ) {
for ( List < String > : : Element * E = manifest_sections [ " application_tags " ] . front ( ) ; E ; E = E - > next ( ) ) {
String to_add = E - > get ( ) . strip_edges ( ) ;
base + = " " + to_add + " " ;
}
}
base + = " > \n " ;
new_file + = base ;
}
} else {
new_file + = l + " \n " ;
}
}
}
}
FileAccessRef f = FileAccess : : open ( " res://android/build/AndroidManifest.xml " , FileAccess : : WRITE ) ;
f - > store_string ( new_file ) ;
f - > close ( ) ;
}
}
virtual Error export_project ( const Ref < EditorExportPreset > & p_preset , bool p_debug , const String & p_path , int p_flags = 0 ) {
ExportNotifier notifier ( * this , p_preset , p_debug , p_path , p_flags ) ;
@ -1481,21 +1765,86 @@ public:
EditorProgress ep ( " export " , " Exporting for Android " , 105 ) ;
if ( p_debug )
src_apk = p_preset - > get ( " custom_package/debug " ) ;
else
src_apk = p_preset - > get ( " custom_package/release " ) ;
if ( bool ( p_preset - > get ( " custom_package/use_custom_build " ) ) ) { //custom build
//re-generate build.gradle and AndroidManifest.xml
src_apk = src_apk . strip_edges ( ) ;
if ( src_apk = = " " ) {
{ //test that installed build version is alright
FileAccessRef f = FileAccess : : open ( " res://android/.build_version " , FileAccess : : READ ) ;
if ( ! f ) {
EditorNode : : get_singleton ( ) - > show_warning ( TTR ( " Trying to build from a custom built template, but no version info for it exists. Please reinstall from the 'Project' menu. " ) ) ;
return ERR_UNCONFIGURED ;
}
String version = f - > get_line ( ) . strip_edges ( ) ;
if ( version ! = VERSION_FULL_CONFIG ) {
EditorNode : : get_singleton ( ) - > show_warning ( vformat ( TTR ( " Android build version mismatch: \n Template installed: %s \n Godot Version: %s \n Please reinstall Android build template from 'Project' menu. " ) , version , VERSION_FULL_CONFIG ) ) ;
return ERR_UNCONFIGURED ;
}
}
//build project if custom build is enabled
String sdk_path = EDITOR_GET ( " export/android/custom_build_sdk_path " ) ;
ERR_FAIL_COND_V ( sdk_path = = " " , ERR_UNCONFIGURED ) ;
_update_custom_build_project ( ) ;
OS : : get_singleton ( ) - > set_environment ( " ANDROID_HOME " , sdk_path ) ; //set and overwrite if required
String build_command ;
# ifdef WINDOWS_ENABLED
build_command = " gradlew.bat " ;
# else
build_command = " gradlew " ;
# endif
String build_path = ProjectSettings : : get_singleton ( ) - > get_resource_path ( ) . plus_file ( " android/build " ) ;
build_command = build_path . plus_file ( build_command ) ;
List < String > cmdline ;
cmdline . push_back ( " build " ) ;
cmdline . push_back ( " -p " ) ;
cmdline . push_back ( build_path ) ;
/*{ used for debug
int ec ;
String pipe ;
OS : : get_singleton ( ) - > execute ( build_command , cmdline , true , NULL , NULL , & ec ) ;
print_line ( " exit code: " + itos ( ec ) ) ;
}
*/
int result = EditorNode : : get_singleton ( ) - > execute_and_show_output ( TTR ( " Building Android Project (gradle) " ) , build_command , cmdline ) ;
if ( result ! = 0 ) {
EditorNode : : get_singleton ( ) - > show_warning ( TTR ( " Building of Android project failed, check output for the error. \n Alternatively visit docs.godotengine.org for Android build documentation. " ) ) ;
return ERR_CANT_CREATE ;
}
if ( p_debug ) {
src_apk = find_export_template ( " android_debug.apk " ) ;
src_apk = build_path. plus_file ( " build/outputs/apk/debug/build-debug-unsigned .apk" ) ;
} else {
src_apk = find_export_template ( " android_release.apk " ) ;
src_apk = build_path . plus_file ( " build/outputs/apk/release/build-release-unsigned.apk " ) ;
}
if ( ! FileAccess : : exists ( src_apk ) ) {
EditorNode : : get_singleton ( ) - > show_warning ( TTR ( " No build apk generated at: " ) + " \n " + src_apk ) ;
return ERR_CANT_CREATE ;
}
} else {
if ( p_debug )
src_apk = p_preset - > get ( " custom_package/debug " ) ;
else
src_apk = p_preset - > get ( " custom_package/release " ) ;
src_apk = src_apk . strip_edges ( ) ;
if ( src_apk = = " " ) {
EditorNode : : add_io_error ( " Package not found: " + src_apk ) ;
return ERR_FILE_NOT_FOUND ;
if ( p_debug ) {
src_apk = find_export_template ( " android_debug.apk " ) ;
} else {
src_apk = find_export_template ( " android_release.apk " ) ;
}
if ( src_apk = = " " ) {
EditorNode : : add_io_error ( " Package not found: " + src_apk ) ;
return ERR_FILE_NOT_FOUND ;
}
}
}
@ -1975,6 +2324,8 @@ void register_android_exporter() {
EDITOR_DEF ( " export/android/debug_keystore_user " , " androiddebugkey " ) ;
EDITOR_DEF ( " export/android/debug_keystore_pass " , " android " ) ;
EDITOR_DEF ( " export/android/force_system_user " , false ) ;
EDITOR_DEF ( " export/android/custom_build_sdk_path " , " " ) ;
EditorSettings : : get_singleton ( ) - > add_property_hint ( PropertyInfo ( Variant : : STRING , " export/android/custom_build_sdk_path " , PROPERTY_HINT_GLOBAL_DIR , " *.keystore " ) ) ;
EDITOR_DEF ( " export/android/timestamping_authority_url " , " " ) ;
EDITOR_DEF ( " export/android/shutdown_adb_on_exit " , true ) ;