2016-06-18 14:46:12 +02:00
/*************************************************************************/
/* export.cpp */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
2017-08-27 14:16:55 +02:00
/* https://godotengine.org */
2016-06-18 14:46:12 +02:00
/*************************************************************************/
2020-01-01 11:16:22 +01:00
/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
2016-06-18 14:46:12 +02:00
/* */
/* 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. */
/*************************************************************************/
2017-06-21 11:11:38 +02:00
2014-02-10 02:10:30 +01:00
# include "export.h"
2019-08-09 13:45:30 +02:00
2018-09-11 18:13:45 +02:00
# include "core/io/marshalls.h"
# include "core/io/resource_saver.h"
# include "core/io/zip_io.h"
2019-08-09 13:45:30 +02:00
# include "core/os/dir_access.h"
2018-09-11 18:13:45 +02:00
# include "core/os/file_access.h"
# include "core/os/os.h"
# include "core/project_settings.h"
# include "core/version.h"
2017-03-05 14:21:25 +01:00
# include "editor/editor_export.h"
# include "editor/editor_node.h"
2017-03-05 16:44:50 +01:00
# include "editor/editor_settings.h"
2017-06-23 17:03:41 +02:00
# include "platform/osx/logo.gen.h"
2014-02-10 02:10:30 +01:00
# include "string.h"
2017-07-02 13:23:33 +02:00
# include <sys/stat.h>
2014-02-10 02:10:30 +01:00
class EditorExportPlatformOSX : public EditorExportPlatform {
2017-06-21 11:11:38 +02:00
GDCLASS ( EditorExportPlatformOSX , EditorExportPlatform ) ;
2014-02-10 02:10:30 +01:00
int version_code ;
Ref < ImageTexture > logo ;
2017-06-21 11:11:38 +02:00
void _fix_plist ( const Ref < EditorExportPreset > & p_preset , Vector < uint8_t > & plist , const String & p_binary ) ;
void _make_icon ( const Ref < Image > & p_icon , Vector < uint8_t > & p_data ) ;
2017-10-03 20:09:57 +02:00
2017-07-02 13:23:33 +02:00
Error _code_sign ( const Ref < EditorExportPreset > & p_preset , const String & p_path ) ;
Error _create_dmg ( const String & p_dmg_path , const String & p_pkg_name , const String & p_app_path_name ) ;
2019-11-08 13:38:23 +01:00
void _zip_folder_recursive ( zipFile & p_zip , const String & p_root_path , const String & p_folder , const String & p_pkg_name ) ;
2017-10-03 20:09:57 +02:00
# ifdef OSX_ENABLED
bool use_codesign ( ) const { return true ; }
bool use_dmg ( ) const { return true ; }
# else
bool use_codesign ( ) const { return false ; }
bool use_dmg ( ) const { return false ; }
2017-07-02 13:23:33 +02:00
# endif
2014-02-10 02:10:30 +01:00
protected :
2017-06-21 11:11:38 +02:00
virtual void get_preset_features ( const Ref < EditorExportPreset > & p_preset , List < String > * r_features ) ;
virtual void get_export_options ( List < ExportOption > * r_options ) ;
2014-02-10 02:10:30 +01:00
public :
virtual String get_name ( ) const { return " Mac OSX " ; }
2017-07-19 22:00:46 +02:00
virtual String get_os_name ( ) const { return " OSX " ; }
2014-02-10 02:10:30 +01:00
virtual Ref < Texture > get_logo ( ) const { return logo ; }
2018-10-29 22:18:49 +01:00
virtual List < String > get_binary_extensions ( const Ref < EditorExportPreset > & p_preset ) const {
List < String > list ;
if ( use_dmg ( ) ) {
list . push_back ( " dmg " ) ;
}
list . push_back ( " zip " ) ;
return list ;
}
2017-06-21 11:11:38 +02:00
virtual Error export_project ( const Ref < EditorExportPreset > & p_preset , bool p_debug , const String & p_path , int p_flags = 0 ) ;
2014-02-10 02:10:30 +01:00
2017-06-21 11:11:38 +02:00
virtual bool can_export ( const Ref < EditorExportPreset > & p_preset , String & r_error , bool & r_missing_templates ) const ;
2014-02-10 02:10:30 +01:00
2017-07-19 22:00:46 +02:00
virtual void get_platform_features ( List < String > * r_features ) {
r_features - > push_back ( " pc " ) ;
r_features - > push_back ( " s3tc " ) ;
r_features - > push_back ( " OSX " ) ;
}
2018-08-22 04:56:04 +02:00
virtual void resolve_platform_feature_priorities ( const Ref < EditorExportPreset > & p_preset , Set < String > & p_features ) {
}
2014-02-10 02:10:30 +01:00
EditorExportPlatformOSX ( ) ;
~ EditorExportPlatformOSX ( ) ;
} ;
2017-06-21 11:11:38 +02:00
void EditorExportPlatformOSX : : get_preset_features ( const Ref < EditorExportPreset > & p_preset , List < String > * r_features ) {
2017-09-08 03:39:32 +02:00
if ( p_preset - > get ( " texture_format/s3tc " ) ) {
r_features - > push_back ( " s3tc " ) ;
}
if ( p_preset - > get ( " texture_format/etc " ) ) {
r_features - > push_back ( " etc " ) ;
}
if ( p_preset - > get ( " texture_format/etc2 " ) ) {
r_features - > push_back ( " etc2 " ) ;
}
2017-10-02 21:38:39 +02:00
2018-02-19 12:49:31 +01:00
r_features - > push_back ( " 64 " ) ;
2014-02-10 02:10:30 +01:00
}
2017-06-21 11:11:38 +02:00
void EditorExportPlatformOSX : : get_export_options ( List < ExportOption > * r_options ) {
2020-01-07 13:29:02 +01:00
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : STRING , " custom_template/debug " , PROPERTY_HINT_GLOBAL_FILE , " *.zip " ) , " " ) ) ;
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : STRING , " custom_template/release " , PROPERTY_HINT_GLOBAL_FILE , " *.zip " ) , " " ) ) ;
2017-06-21 11:11:38 +02:00
2018-08-17 03:50:12 +02:00
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : STRING , " application/name " , PROPERTY_HINT_PLACEHOLDER_TEXT , " Game Name " ) , " " ) ) ;
2017-06-21 11:11:38 +02:00
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : STRING , " application/info " ) , " Made with Godot Engine " ) ) ;
2019-05-17 07:49:21 +02:00
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : STRING , " application/icon " , PROPERTY_HINT_FILE , " *.png,*.icns " ) , " " ) ) ;
2018-08-17 03:50:12 +02:00
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : STRING , " application/identifier " , PROPERTY_HINT_PLACEHOLDER_TEXT , " com.example.game " ) , " " ) ) ;
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : STRING , " application/signature " ) , " " ) ) ;
2017-06-21 11:11:38 +02:00
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : STRING , " application/short_version " ) , " 1.0 " ) ) ;
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : STRING , " application/version " ) , " 1.0 " ) ) ;
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : STRING , " application/copyright " ) , " " ) ) ;
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : BOOL , " display/high_res " ) , false ) ) ;
2019-12-13 22:10:23 +01:00
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : STRING , " privacy/camera_usage_description " , PROPERTY_HINT_PLACEHOLDER_TEXT , " Provide a message if you need to use the camera " ) , " " ) ) ;
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : STRING , " privacy/microphone_usage_description " , PROPERTY_HINT_PLACEHOLDER_TEXT , " Provide a message if you need to use the microphone " ) , " " ) ) ;
2017-07-02 13:23:33 +02:00
# ifdef OSX_ENABLED
2019-10-04 17:01:13 +02:00
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : BOOL , " codesign/enable " ) , false ) ) ;
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : STRING , " codesign/identity " , PROPERTY_HINT_PLACEHOLDER_TEXT , " Type: Name (ID) " ) , " " ) ) ;
2019-10-02 20:00:16 +02:00
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : BOOL , " codesign/timestamp " ) , true ) ) ;
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : BOOL , " codesign/hardened_runtime " ) , true ) ) ;
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : STRING , " codesign/entitlements " , PROPERTY_HINT_GLOBAL_FILE , " *.plist " ) , " " ) ) ;
2019-10-04 17:01:13 +02:00
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : POOL_STRING_ARRAY , " codesign/custom_options " ) , PoolStringArray ( ) ) ) ;
2017-07-02 13:23:33 +02:00
# endif
2017-09-08 03:39:32 +02:00
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : BOOL , " texture_format/s3tc " ) , true ) ) ;
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : BOOL , " texture_format/etc " ) , false ) ) ;
r_options - > push_back ( ExportOption ( PropertyInfo ( Variant : : BOOL , " texture_format/etc2 " ) , false ) ) ;
2014-02-10 02:10:30 +01:00
}
2018-11-26 15:01:38 +01:00
void _rgba8_to_packbits_encode ( int p_ch , int p_size , PoolVector < uint8_t > & p_source , Vector < uint8_t > & p_dest ) {
int src_len = p_size * p_size ;
Vector < uint8_t > result ;
result . resize ( src_len * 1.25 ) ; //temp vector for rle encoded data, make it 25% larger for worst case scenario
int res_size = 0 ;
uint8_t buf [ 128 ] ;
int buf_size = 0 ;
int i = 0 ;
while ( i < src_len ) {
uint8_t cur = p_source . read ( ) [ i * 4 + p_ch ] ;
if ( i < src_len - 2 ) {
if ( ( p_source . read ( ) [ ( i + 1 ) * 4 + p_ch ] = = cur ) & & ( p_source . read ( ) [ ( i + 2 ) * 4 + p_ch ] = = cur ) ) {
if ( buf_size > 0 ) {
result . write [ res_size + + ] = ( uint8_t ) ( buf_size - 1 ) ;
copymem ( & result . write [ res_size ] , & buf , buf_size ) ;
res_size + = buf_size ;
buf_size = 0 ;
}
uint8_t lim = i + 130 > = src_len ? src_len - i - 1 : 130 ;
bool hit_lim = true ;
for ( int j = 3 ; j < = lim ; j + + ) {
if ( p_source . read ( ) [ ( i + j ) * 4 + p_ch ] ! = cur ) {
hit_lim = false ;
i = i + j - 1 ;
result . write [ res_size + + ] = ( uint8_t ) ( j - 3 + 0x80 ) ;
result . write [ res_size + + ] = cur ;
break ;
}
}
if ( hit_lim ) {
result . write [ res_size + + ] = ( uint8_t ) ( lim - 3 + 0x80 ) ;
result . write [ res_size + + ] = cur ;
i = i + lim ;
}
} else {
buf [ buf_size + + ] = cur ;
if ( buf_size = = 128 ) {
result . write [ res_size + + ] = ( uint8_t ) ( buf_size - 1 ) ;
copymem ( & result . write [ res_size ] , & buf , buf_size ) ;
res_size + = buf_size ;
buf_size = 0 ;
}
}
} else {
buf [ buf_size + + ] = cur ;
result . write [ res_size + + ] = ( uint8_t ) ( buf_size - 1 ) ;
copymem ( & result . write [ res_size ] , & buf , buf_size ) ;
res_size + = buf_size ;
buf_size = 0 ;
}
i + + ;
}
int ofs = p_dest . size ( ) ;
p_dest . resize ( p_dest . size ( ) + res_size ) ;
copymem ( & p_dest . write [ ofs ] , result . ptr ( ) , res_size ) ;
}
2017-06-21 11:11:38 +02:00
void EditorExportPlatformOSX : : _make_icon ( const Ref < Image > & p_icon , Vector < uint8_t > & p_data ) {
2014-02-10 02:10:30 +01:00
2017-06-21 11:11:38 +02:00
Ref < ImageTexture > it = memnew ( ImageTexture ) ;
2014-02-10 02:10:30 +01:00
Vector < uint8_t > data ;
data . resize ( 8 ) ;
2018-07-25 03:11:03 +02:00
data . write [ 0 ] = ' i ' ;
data . write [ 1 ] = ' c ' ;
data . write [ 2 ] = ' n ' ;
data . write [ 3 ] = ' s ' ;
2014-02-10 02:10:30 +01:00
2018-11-26 15:01:38 +01:00
struct MacOSIconInfo {
const char * name ;
const char * mask_name ;
bool is_png ;
int size ;
} ;
static const MacOSIconInfo icon_infos [ ] = {
{ " ic10 " , " " , true , 1024 } , //1024x1024 32-bit PNG and 512x512@2x 32-bit "retina" PNG
{ " ic09 " , " " , true , 512 } , //512× 512 32-bit PNG
{ " ic14 " , " " , true , 512 } , //256x256@2x 32-bit "retina" PNG
{ " ic08 " , " " , true , 256 } , //256× 256 32-bit PNG
{ " ic13 " , " " , true , 256 } , //128x128@2x 32-bit "retina" PNG
{ " ic07 " , " " , true , 128 } , //128x128 32-bit PNG
{ " ic12 " , " " , true , 64 } , //32x32@2x 32-bit "retina" PNG
{ " ic11 " , " " , true , 32 } , //16x16@2x 32-bit "retina" PNG
{ " il32 " , " l8mk " , false , 32 } , //32x32 24-bit RLE + 8-bit uncompressed mask
{ " is32 " , " s8mk " , false , 16 } //16x16 24-bit RLE + 8-bit uncompressed mask
} ;
2019-09-22 18:45:08 +02:00
for ( uint64_t i = 0 ; i < ( sizeof ( icon_infos ) / sizeof ( icon_infos [ 0 ] ) ) ; + + i ) {
2017-06-21 11:11:38 +02:00
Ref < Image > copy = p_icon ; // does this make sense? doesn't this just increase the reference count instead of making a copy? Do we even need a copy?
copy - > convert ( Image : : FORMAT_RGBA8 ) ;
2018-11-26 15:01:38 +01:00
copy - > resize ( icon_infos [ i ] . size , icon_infos [ i ] . size ) ;
if ( icon_infos [ i ] . is_png ) {
2019-08-09 13:45:30 +02:00
// Encode PNG icon.
2018-11-26 15:01:38 +01:00
it - > create_from_image ( copy ) ;
String path = EditorSettings : : get_singleton ( ) - > get_cache_dir ( ) . plus_file ( " icon.png " ) ;
ResourceSaver : : save ( path , it ) ;
FileAccess * f = FileAccess : : open ( path , FileAccess : : READ ) ;
2019-08-09 13:45:30 +02:00
if ( ! f ) {
// Clean up generated file.
DirAccess : : remove_file_or_error ( path ) ;
ERR_FAIL ( ) ;
}
2018-11-26 15:01:38 +01:00
int ofs = data . size ( ) ;
uint32_t len = f - > get_len ( ) ;
data . resize ( data . size ( ) + len + 8 ) ;
f - > get_buffer ( & data . write [ ofs + 8 ] , len ) ;
memdelete ( f ) ;
len + = 8 ;
len = BSWAP32 ( len ) ;
copymem ( & data . write [ ofs ] , icon_infos [ i ] . name , 4 ) ;
encode_uint32 ( len , & data . write [ ofs + 4 ] ) ;
2019-08-09 13:45:30 +02:00
// Clean up generated file.
DirAccess : : remove_file_or_error ( path ) ;
2018-11-26 15:01:38 +01:00
} else {
PoolVector < uint8_t > src_data = copy - > get_data ( ) ;
//encode 24bit RGB RLE icon
{
int ofs = data . size ( ) ;
data . resize ( data . size ( ) + 8 ) ;
_rgba8_to_packbits_encode ( 0 , icon_infos [ i ] . size , src_data , data ) ; // encode R
_rgba8_to_packbits_encode ( 1 , icon_infos [ i ] . size , src_data , data ) ; // encode G
_rgba8_to_packbits_encode ( 2 , icon_infos [ i ] . size , src_data , data ) ; // encode B
int len = data . size ( ) - ofs ;
len = BSWAP32 ( len ) ;
copymem ( & data . write [ ofs ] , icon_infos [ i ] . name , 4 ) ;
encode_uint32 ( len , & data . write [ ofs + 4 ] ) ;
}
//encode 8bit mask uncompressed icon
{
int ofs = data . size ( ) ;
int len = copy - > get_width ( ) * copy - > get_height ( ) ;
data . resize ( data . size ( ) + len + 8 ) ;
for ( int j = 0 ; j < len ; j + + ) {
data . write [ ofs + 8 + j ] = src_data . read ( ) [ j * 4 + 3 ] ;
}
len + = 8 ;
len = BSWAP32 ( len ) ;
copymem ( & data . write [ ofs ] , icon_infos [ i ] . mask_name , 4 ) ;
encode_uint32 ( len , & data . write [ ofs + 4 ] ) ;
}
}
2014-02-10 02:10:30 +01:00
}
uint32_t total_len = data . size ( ) ;
total_len = BSWAP32 ( total_len ) ;
2018-07-25 03:11:03 +02:00
encode_uint32 ( total_len , & data . write [ 4 ] ) ;
2014-02-10 02:10:30 +01:00
2017-06-21 11:11:38 +02:00
p_data = data ;
2014-02-10 02:10:30 +01:00
}
2017-06-21 11:11:38 +02:00
void EditorExportPlatformOSX : : _fix_plist ( const Ref < EditorExportPreset > & p_preset , Vector < uint8_t > & plist , const String & p_binary ) {
2014-02-10 02:10:30 +01:00
String str ;
String strnew ;
2017-06-21 11:11:38 +02:00
str . parse_utf8 ( ( const char * ) plist . ptr ( ) , plist . size ( ) ) ;
Vector < String > lines = str . split ( " \n " ) ;
for ( int i = 0 ; i < lines . size ( ) ; i + + ) {
if ( lines [ i ] . find ( " $binary " ) ! = - 1 ) {
strnew + = lines [ i ] . replace ( " $binary " , p_binary ) + " \n " ;
} else if ( lines [ i ] . find ( " $name " ) ! = - 1 ) {
strnew + = lines [ i ] . replace ( " $name " , p_binary ) + " \n " ;
} else if ( lines [ i ] . find ( " $info " ) ! = - 1 ) {
strnew + = lines [ i ] . replace ( " $info " , p_preset - > get ( " application/info " ) ) + " \n " ;
} else if ( lines [ i ] . find ( " $identifier " ) ! = - 1 ) {
strnew + = lines [ i ] . replace ( " $identifier " , p_preset - > get ( " application/identifier " ) ) + " \n " ;
} else if ( lines [ i ] . find ( " $short_version " ) ! = - 1 ) {
strnew + = lines [ i ] . replace ( " $short_version " , p_preset - > get ( " application/short_version " ) ) + " \n " ;
} else if ( lines [ i ] . find ( " $version " ) ! = - 1 ) {
strnew + = lines [ i ] . replace ( " $version " , p_preset - > get ( " application/version " ) ) + " \n " ;
} else if ( lines [ i ] . find ( " $signature " ) ! = - 1 ) {
strnew + = lines [ i ] . replace ( " $signature " , p_preset - > get ( " application/signature " ) ) + " \n " ;
} else if ( lines [ i ] . find ( " $copyright " ) ! = - 1 ) {
strnew + = lines [ i ] . replace ( " $copyright " , p_preset - > get ( " application/copyright " ) ) + " \n " ;
} else if ( lines [ i ] . find ( " $highres " ) ! = - 1 ) {
strnew + = lines [ i ] . replace ( " $highres " , p_preset - > get ( " display/high_res " ) ? " <true/> " : " <false/> " ) + " \n " ;
2019-12-13 22:10:23 +01:00
} else if ( lines [ i ] . find ( " $camera_usage_description " ) ! = - 1 ) {
String description = p_preset - > get ( " privacy/camera_usage_description " ) ;
strnew + = lines [ i ] . replace ( " $camera_usage_description " , description ) + " \n " ;
} else if ( lines [ i ] . find ( " $microphone_usage_description " ) ! = - 1 ) {
String description = p_preset - > get ( " privacy/microphone_usage_description " ) ;
strnew + = lines [ i ] . replace ( " $microphone_usage_description " , description ) + " \n " ;
2014-02-10 02:10:30 +01:00
} else {
2017-06-21 11:11:38 +02:00
strnew + = lines [ i ] + " \n " ;
2014-02-10 02:10:30 +01:00
}
}
CharString cs = strnew . utf8 ( ) ;
2017-07-05 16:14:05 +02:00
plist . resize ( cs . size ( ) - 1 ) ;
for ( int i = 0 ; i < cs . size ( ) - 1 ; i + + ) {
2018-07-25 03:11:03 +02:00
plist . write [ i ] = cs [ i ] ;
2014-02-10 02:10:30 +01:00
}
}
2017-07-02 13:23:33 +02:00
/**
If we ' re running the OSX version of the Godot editor we ' ll :
- export our application bundle to a temporary folder
- attempt to code sign it
- and then wrap it up in a DMG
* */
Error EditorExportPlatformOSX : : _code_sign ( const Ref < EditorExportPreset > & p_preset , const String & p_path ) {
2019-11-08 13:38:23 +01:00
# ifdef OSX_ENABLED
2017-07-02 13:23:33 +02:00
List < String > args ;
2017-10-03 20:09:57 +02:00
2019-10-02 20:00:16 +02:00
if ( p_preset - > get ( " codesign/timestamp " ) ) {
args . push_back ( " --timestamp " ) ;
}
if ( p_preset - > get ( " codesign/hardened_runtime " ) ) {
args . push_back ( " --options " ) ;
args . push_back ( " runtime " ) ;
}
2019-11-08 13:38:23 +01:00
if ( ( p_preset - > get ( " codesign/entitlements " ) ! = " " ) & & ( p_path . get_extension ( ) ! = " dmg " ) ) {
2019-10-02 20:00:16 +02:00
args . push_back ( " --entitlements " ) ;
2017-07-02 13:23:33 +02:00
args . push_back ( p_preset - > get ( " codesign/entitlements " ) ) ;
}
2019-10-04 17:01:13 +02:00
PoolStringArray user_args = p_preset - > get ( " codesign/custom_options " ) ;
for ( int i = 0 ; i < user_args . size ( ) ; i + + ) {
String user_arg = user_args [ i ] . strip_edges ( ) ;
if ( ! user_arg . empty ( ) ) {
args . push_back ( user_arg ) ;
}
}
2017-07-02 13:23:33 +02:00
args . push_back ( " -s " ) ;
args . push_back ( p_preset - > get ( " codesign/identity " ) ) ;
2019-10-04 17:01:13 +02:00
2017-07-02 13:23:33 +02:00
args . push_back ( " -v " ) ; /* provide some more feedback */
2019-10-04 17:01:13 +02:00
2017-07-02 13:23:33 +02:00
args . push_back ( p_path ) ;
2017-10-03 20:09:57 +02:00
String str ;
2017-10-13 16:45:24 +02:00
Error err = OS : : get_singleton ( ) - > execute ( " codesign " , args , true , NULL , & str , NULL , true ) ;
2017-10-03 20:09:57 +02:00
ERR_FAIL_COND_V ( err ! = OK , err ) ;
2019-10-04 17:01:13 +02:00
print_line ( " codesign ( " + p_path + " ): " + str ) ;
2017-10-03 20:09:57 +02:00
if ( str . find ( " no identity found " ) ! = - 1 ) {
EditorNode : : add_io_error ( " codesign: no identity found " ) ;
return FAILED ;
}
2019-10-02 20:00:16 +02:00
if ( ( str . find ( " unrecognized blob type " ) ! = - 1 ) | | ( str . find ( " cannot read entitlement data " ) ! = - 1 ) ) {
EditorNode : : add_io_error ( " codesign: invalid entitlements file " ) ;
return FAILED ;
}
2019-11-08 13:38:23 +01:00
# endif
2017-07-02 13:23:33 +02:00
return OK ;
}
Error EditorExportPlatformOSX : : _create_dmg ( const String & p_dmg_path , const String & p_pkg_name , const String & p_app_path_name ) {
List < String > args ;
2017-10-03 20:09:57 +02:00
2019-10-02 20:00:16 +02:00
if ( FileAccess : : exists ( p_dmg_path ) ) {
OS : : get_singleton ( ) - > move_to_trash ( p_dmg_path ) ;
}
2017-10-03 20:09:57 +02:00
2017-07-02 13:23:33 +02:00
args . push_back ( " create " ) ;
args . push_back ( p_dmg_path ) ;
args . push_back ( " -volname " ) ;
args . push_back ( p_pkg_name ) ;
args . push_back ( " -fs " ) ;
args . push_back ( " HFS+ " ) ;
args . push_back ( " -srcfolder " ) ;
args . push_back ( p_app_path_name ) ;
2017-10-03 20:09:57 +02:00
String str ;
2017-10-13 16:45:24 +02:00
Error err = OS : : get_singleton ( ) - > execute ( " hdiutil " , args , true , NULL , & str , NULL , true ) ;
2017-10-03 20:09:57 +02:00
ERR_FAIL_COND_V ( err ! = OK , err ) ;
print_line ( " hdiutil returned: " + str ) ;
if ( str . find ( " create failed " ) ! = - 1 ) {
if ( str . find ( " File exists " ) ! = - 1 ) {
EditorNode : : add_io_error ( " hdiutil: create failed - file exists " ) ;
} else {
EditorNode : : add_io_error ( " hdiutil: create failed " ) ;
}
return FAILED ;
}
2017-07-02 13:23:33 +02:00
return OK ;
}
2017-06-21 11:11:38 +02:00
Error EditorExportPlatformOSX : : export_project ( const Ref < EditorExportPreset > & p_preset , bool p_debug , const String & p_path , int p_flags ) {
2017-10-02 17:01:43 +02:00
ExportNotifier notifier ( * this , p_preset , p_debug , p_path , p_flags ) ;
2014-02-10 02:10:30 +01:00
2017-07-02 13:23:33 +02:00
String src_pkg_name ;
2014-02-10 02:10:30 +01:00
2018-12-19 19:50:40 +01:00
EditorProgress ep ( " export " , " Exporting for OSX " , 3 , true ) ;
2014-02-10 02:10:30 +01:00
2015-11-30 01:26:51 +01:00
if ( p_debug )
2020-01-07 13:29:02 +01:00
src_pkg_name = p_preset - > get ( " custom_template/debug " ) ;
2015-11-30 01:26:51 +01:00
else
2020-01-07 13:29:02 +01:00
src_pkg_name = p_preset - > get ( " custom_template/release " ) ;
2015-11-30 01:26:51 +01:00
2017-07-02 13:23:33 +02:00
if ( src_pkg_name = = " " ) {
2015-11-30 01:26:51 +01:00
String err ;
2017-07-02 13:23:33 +02:00
src_pkg_name = find_export_template ( " osx.zip " , & err ) ;
if ( src_pkg_name = = " " ) {
2015-11-30 01:26:51 +01:00
EditorNode : : add_io_error ( err ) ;
return ERR_FILE_NOT_FOUND ;
}
2014-02-10 02:10:30 +01:00
}
2019-03-06 13:20:18 +01:00
if ( ! DirAccess : : exists ( p_path . get_base_dir ( ) ) ) {
2019-03-05 08:52:45 +01:00
return ERR_FILE_BAD_PATH ;
}
2017-06-21 11:11:38 +02:00
FileAccess * src_f = NULL ;
2014-02-10 02:10:30 +01:00
zlib_filefunc_def io = zipio_create_io_from_file ( & src_f ) ;
2018-12-19 19:50:40 +01:00
if ( ep . step ( " Creating app " , 0 ) ) {
return ERR_SKIP ;
}
2014-02-10 02:10:30 +01:00
2017-07-02 13:23:33 +02:00
unzFile src_pkg_zip = unzOpen2 ( src_pkg_name . utf8 ( ) . get_data ( ) , & io ) ;
if ( ! src_pkg_zip ) {
2014-02-10 02:10:30 +01:00
2017-07-02 13:23:33 +02:00
EditorNode : : add_io_error ( " Could not find template app to export: \n " + src_pkg_name ) ;
2014-02-10 02:10:30 +01:00
return ERR_FILE_NOT_FOUND ;
}
2017-07-02 13:23:33 +02:00
int ret = unzGoToFirstFile ( src_pkg_zip ) ;
2014-02-10 02:10:30 +01:00
2018-02-19 12:49:31 +01:00
String binary_to_use = " godot_osx_ " + String ( p_debug ? " debug " : " release " ) + " .64 " ;
2014-02-10 02:10:30 +01:00
String pkg_name ;
2017-06-21 11:11:38 +02:00
if ( p_preset - > get ( " application/name " ) ! = " " )
pkg_name = p_preset - > get ( " application/name " ) ; // app_name
2017-07-19 22:00:46 +02:00
else if ( String ( ProjectSettings : : get_singleton ( ) - > get ( " application/config/name " ) ) ! = " " )
pkg_name = String ( ProjectSettings : : get_singleton ( ) - > get ( " application/config/name " ) ) ;
2014-02-10 02:10:30 +01:00
else
2017-06-21 11:11:38 +02:00
pkg_name = " Unnamed " ;
2014-02-10 02:10:30 +01:00
2020-01-10 19:08:01 +01:00
String pkg_name_safe = OS : : get_singleton ( ) - > get_safe_dir_name ( pkg_name ) ;
2017-10-03 20:09:57 +02:00
Error err = OK ;
String tmp_app_path_name = " " ;
2020-01-10 11:28:32 +01:00
DirAccess * tmp_app_path = NULL ;
2018-10-29 22:18:49 +01:00
String export_format = use_dmg ( ) & & p_path . ends_with ( " dmg " ) ? " dmg " : " zip " ;
2017-07-02 13:23:33 +02:00
2019-11-08 13:38:23 +01:00
// Create our application bundle.
tmp_app_path_name = EditorSettings : : get_singleton ( ) - > get_cache_dir ( ) . plus_file ( pkg_name + " .app " ) ;
print_line ( " Exporting to " + tmp_app_path_name ) ;
tmp_app_path = DirAccess : : create_for_path ( tmp_app_path_name ) ;
if ( ! tmp_app_path ) {
err = ERR_CANT_CREATE ;
}
2017-07-02 13:23:33 +02:00
2019-11-08 13:38:23 +01:00
// Create our folder structure.
if ( err = = OK ) {
print_line ( " Creating " + tmp_app_path_name + " /Contents/MacOS " ) ;
err = tmp_app_path - > make_dir_recursive ( tmp_app_path_name + " /Contents/MacOS " ) ;
}
2018-01-04 20:36:44 +01:00
2019-11-08 13:38:23 +01:00
if ( err = = OK ) {
print_line ( " Creating " + tmp_app_path_name + " /Contents/Frameworks " ) ;
err = tmp_app_path - > make_dir_recursive ( tmp_app_path_name + " /Contents/Frameworks " ) ;
}
if ( err = = OK ) {
print_line ( " Creating " + tmp_app_path_name + " /Contents/Resources " ) ;
err = tmp_app_path - > make_dir_recursive ( tmp_app_path_name + " /Contents/Resources " ) ;
2017-10-03 20:09:57 +02:00
}
2017-07-02 13:23:33 +02:00
2019-11-08 13:38:23 +01:00
// Now process our template.
2016-07-09 00:46:10 +02:00
bool found_binary = false ;
2017-07-02 13:23:33 +02:00
int total_size = 0 ;
2016-07-09 00:46:10 +02:00
2017-10-03 20:09:57 +02:00
while ( ret = = UNZ_OK & & err = = OK ) {
2017-07-02 13:23:33 +02:00
bool is_execute = false ;
2014-02-10 02:10:30 +01:00
2019-11-08 13:38:23 +01:00
// Get filename.
2014-02-10 02:10:30 +01:00
unz_file_info info ;
char fname [ 16384 ] ;
2017-07-02 13:23:33 +02:00
ret = unzGetCurrentFileInfo ( src_pkg_zip , & info , fname , 16384 , NULL , 0 , NULL , 0 ) ;
2014-02-10 02:10:30 +01:00
2017-06-21 11:11:38 +02:00
String file = fname ;
2014-02-10 02:10:30 +01:00
Vector < uint8_t > data ;
data . resize ( info . uncompressed_size ) ;
2019-11-08 13:38:23 +01:00
// Read.
2017-07-02 13:23:33 +02:00
unzOpenCurrentFile ( src_pkg_zip ) ;
2017-11-25 04:07:54 +01:00
unzReadCurrentFile ( src_pkg_zip , data . ptrw ( ) , data . size ( ) ) ;
2017-07-02 13:23:33 +02:00
unzCloseCurrentFile ( src_pkg_zip ) ;
2014-02-10 02:10:30 +01:00
2019-11-08 13:38:23 +01:00
// Write.
2017-06-21 11:11:38 +02:00
file = file . replace_first ( " osx_template.app/ " , " " ) ;
2014-02-10 02:10:30 +01:00
2017-06-21 11:11:38 +02:00
if ( file = = " Contents/Info.plist " ) {
_fix_plist ( p_preset , data , pkg_name ) ;
2014-02-10 02:10:30 +01:00
}
if ( file . begins_with ( " Contents/MacOS/godot_ " ) ) {
2017-06-21 11:11:38 +02:00
if ( file ! = " Contents/MacOS/ " + binary_to_use ) {
2017-07-02 13:23:33 +02:00
ret = unzGoToNextFile ( src_pkg_zip ) ;
2019-11-08 13:38:23 +01:00
continue ; // skip
2014-02-10 02:10:30 +01:00
}
2016-07-09 00:46:10 +02:00
found_binary = true ;
2017-07-02 13:23:33 +02:00
is_execute = true ;
2017-06-21 11:11:38 +02:00
file = " Contents/MacOS/ " + pkg_name ;
2014-02-10 02:10:30 +01:00
}
2017-06-21 11:11:38 +02:00
if ( file = = " Contents/Resources/icon.icns " ) {
2019-11-08 13:38:23 +01:00
// See if there is an icon.
2017-06-21 11:11:38 +02:00
String iconpath ;
if ( p_preset - > get ( " application/icon " ) ! = " " )
iconpath = p_preset - > get ( " application/icon " ) ;
else
2017-07-19 22:00:46 +02:00
iconpath = ProjectSettings : : get_singleton ( ) - > get ( " application/config/icon " ) ;
2018-08-24 09:35:07 +02:00
2017-06-21 11:11:38 +02:00
if ( iconpath ! = " " ) {
2019-05-17 07:49:21 +02:00
if ( iconpath . get_extension ( ) = = " icns " ) {
FileAccess * icon = FileAccess : : open ( iconpath , FileAccess : : READ ) ;
if ( icon ) {
data . resize ( icon - > get_len ( ) ) ;
icon - > get_buffer ( & data . write [ 0 ] , icon - > get_len ( ) ) ;
icon - > close ( ) ;
memdelete ( icon ) ;
}
} else {
Ref < Image > icon ;
icon . instance ( ) ;
icon - > load ( iconpath ) ;
if ( ! icon - > empty ( ) ) {
_make_icon ( icon , data ) ;
}
2014-02-10 02:10:30 +01:00
}
}
}
2017-07-02 13:23:33 +02:00
if ( data . size ( ) > 0 ) {
2020-01-10 19:08:01 +01:00
if ( file . find ( " /data.mono.osx.64.release_debug/ " ) ! = - 1 ) {
if ( ! p_debug ) {
ret = unzGoToNextFile ( src_pkg_zip ) ;
2019-11-08 13:38:23 +01:00
continue ; // skip
2020-01-10 19:08:01 +01:00
}
file = file . replace ( " /data.mono.osx.64.release_debug/ " , " /data_ " + pkg_name_safe + " / " ) ;
}
if ( file . find ( " /data.mono.osx.64.release/ " ) ! = - 1 ) {
if ( p_debug ) {
ret = unzGoToNextFile ( src_pkg_zip ) ;
2019-11-08 13:38:23 +01:00
continue ; // skip
2020-01-10 19:08:01 +01:00
}
file = file . replace ( " /data.mono.osx.64.release/ " , " /data_ " + pkg_name_safe + " / " ) ;
}
2017-07-02 13:23:33 +02:00
print_line ( " ADDING: " + file + " size: " + itos ( data . size ( ) ) ) ;
total_size + = data . size ( ) ;
2019-11-08 13:38:23 +01:00
// Write it into our application bundle.
file = tmp_app_path_name . plus_file ( file ) ;
if ( err = = OK ) {
err = tmp_app_path - > make_dir_recursive ( file . get_base_dir ( ) ) ;
}
if ( err = = OK ) {
FileAccess * f = FileAccess : : open ( file , FileAccess : : WRITE ) ;
if ( f ) {
f - > store_buffer ( data . ptr ( ) , data . size ( ) ) ;
f - > close ( ) ;
if ( is_execute ) {
// chmod with 0755 if the file is executable.
FileAccess : : set_unix_permissions ( file , 0755 ) ;
2017-10-03 20:09:57 +02:00
}
2019-11-08 13:38:23 +01:00
memdelete ( f ) ;
} else {
err = ERR_CANT_CREATE ;
2017-10-03 20:09:57 +02:00
}
2017-07-02 13:23:33 +02:00
}
}
ret = unzGoToNextFile ( src_pkg_zip ) ;
}
2019-11-08 13:38:23 +01:00
// We're done with our source zip.
2017-07-02 13:23:33 +02:00
unzClose ( src_pkg_zip ) ;
if ( ! found_binary ) {
ERR_PRINTS ( " Requested template binary ' " + binary_to_use + " ' not found. It might be missing from your template archive. " ) ;
2017-10-03 20:09:57 +02:00
err = ERR_FILE_NOT_FOUND ;
2017-07-02 13:23:33 +02:00
}
2017-10-03 20:09:57 +02:00
if ( err = = OK ) {
2018-12-19 19:50:40 +01:00
if ( ep . step ( " Making PKG " , 1 ) ) {
return ERR_SKIP ;
}
2017-07-02 13:23:33 +02:00
2019-11-08 13:38:23 +01:00
String pack_path = tmp_app_path_name + " /Contents/Resources/ " + pkg_name + " .pck " ;
Vector < SharedObject > shared_objects ;
err = save_pack ( p_preset , pack_path , & shared_objects ) ;
2017-07-02 13:23:33 +02:00
2019-11-08 13:38:23 +01:00
// See if we can code sign our new package.
bool sign_enabled = p_preset - > get ( " codesign/enable " ) ;
2018-01-04 20:36:44 +01:00
2019-11-08 13:38:23 +01:00
if ( err = = OK ) {
DirAccess * da = DirAccess : : create ( DirAccess : : ACCESS_FILESYSTEM ) ;
for ( int i = 0 ; i < shared_objects . size ( ) ; i + + ) {
err = da - > copy ( shared_objects [ i ] . path , tmp_app_path_name + " /Contents/Frameworks/ " + shared_objects [ i ] . path . get_file ( ) ) ;
if ( err = = OK & & sign_enabled ) {
err = _code_sign ( p_preset , tmp_app_path_name + " /Contents/Frameworks/ " + shared_objects [ i ] . path . get_file ( ) ) ;
2018-01-04 20:36:44 +01:00
}
}
2019-11-08 13:38:23 +01:00
memdelete ( da ) ;
}
2018-01-04 20:36:44 +01:00
2019-11-08 13:38:23 +01:00
if ( err = = OK & & sign_enabled ) {
if ( ep . step ( " Code signing bundle " , 2 ) ) {
return ERR_SKIP ;
2017-10-03 20:09:57 +02:00
}
2019-11-08 13:38:23 +01:00
err = _code_sign ( p_preset , tmp_app_path_name + " /Contents/MacOS/ " + pkg_name ) ;
}
2017-07-02 13:23:33 +02:00
2019-11-08 13:38:23 +01:00
if ( export_format = = " dmg " ) {
// Create a DMG.
2017-10-03 20:09:57 +02:00
if ( err = = OK ) {
2018-12-19 19:50:40 +01:00
if ( ep . step ( " Making DMG " , 3 ) ) {
return ERR_SKIP ;
}
2017-10-03 20:09:57 +02:00
err = _create_dmg ( p_path , pkg_name , tmp_app_path_name ) ;
2017-07-02 13:23:33 +02:00
}
2019-11-08 13:38:23 +01:00
// Sign DMG.
if ( err = = OK & & sign_enabled ) {
if ( ep . step ( " Code signing DMG " , 3 ) ) {
return ERR_SKIP ;
2017-10-03 20:09:57 +02:00
}
2019-11-08 13:38:23 +01:00
err = _code_sign ( p_preset , p_path ) ;
2018-03-06 16:13:18 +01:00
}
2019-11-08 13:38:23 +01:00
} else {
// Create ZIP.
2018-03-06 16:13:18 +01:00
if ( err = = OK ) {
2019-11-08 13:38:23 +01:00
if ( ep . step ( " Making ZIP " , 3 ) ) {
return ERR_SKIP ;
}
if ( FileAccess : : exists ( p_path ) ) {
OS : : get_singleton ( ) - > move_to_trash ( p_path ) ;
2018-01-04 19:42:29 +01:00
}
2019-08-09 13:45:30 +02:00
2019-11-08 13:38:23 +01:00
FileAccess * dst_f = nullptr ;
zlib_filefunc_def io_dst = zipio_create_io_from_file ( & dst_f ) ;
zipFile zip = zipOpen2 ( p_path . utf8 ( ) . get_data ( ) , APPEND_STATUS_CREATE , nullptr , & io_dst ) ;
_zip_folder_recursive ( zip , EditorSettings : : get_singleton ( ) - > get_cache_dir ( ) , pkg_name + " .app " , pkg_name ) ;
zipClose ( zip , nullptr ) ;
}
2014-02-10 02:10:30 +01:00
}
2019-11-08 13:38:23 +01:00
// Clean up temporary .app dir.
OS : : get_singleton ( ) - > move_to_trash ( tmp_app_path_name ) ;
2016-07-09 00:46:10 +02:00
}
2018-03-06 16:13:18 +01:00
return err ;
2014-02-10 02:10:30 +01:00
}
2019-11-08 13:38:23 +01:00
void EditorExportPlatformOSX : : _zip_folder_recursive ( zipFile & p_zip , const String & p_root_path , const String & p_folder , const String & p_pkg_name ) {
String dir = p_root_path . plus_file ( p_folder ) ;
DirAccess * da = DirAccess : : open ( dir ) ;
da - > list_dir_begin ( ) ;
String f ;
while ( ( f = da - > get_next ( ) ) ! = " " ) {
if ( f = = " . " | | f = = " .. " ) {
continue ;
}
if ( da - > current_is_dir ( ) ) {
_zip_folder_recursive ( p_zip , p_root_path , p_folder . plus_file ( f ) , p_pkg_name ) ;
} else {
bool is_executable = ( p_folder . ends_with ( " MacOS " ) & & ( f = = p_pkg_name ) ) ;
OS : : Time time = OS : : get_singleton ( ) - > get_time ( ) ;
OS : : Date date = OS : : get_singleton ( ) - > get_date ( ) ;
zip_fileinfo zipfi ;
zipfi . tmz_date . tm_hour = time . hour ;
zipfi . tmz_date . tm_mday = date . day ;
zipfi . tmz_date . tm_min = time . min ;
zipfi . tmz_date . tm_mon = date . month ;
zipfi . tmz_date . tm_sec = time . sec ;
zipfi . tmz_date . tm_year = date . year ;
zipfi . dosDate = 0 ;
2020-06-20 10:04:18 +02:00
// 0100000: regular file type
// 0000755: permissions rwxr-xr-x
// 0000644: permissions rw-r--r--
zipfi . external_fa = ( is_executable ? 0100755 : 0100644 ) < < 16L ;
2019-11-08 13:38:23 +01:00
zipfi . internal_fa = 0 ;
zipOpenNewFileInZip4 ( p_zip ,
p_folder . plus_file ( f ) . utf8 ( ) . get_data ( ) ,
& zipfi ,
nullptr ,
0 ,
nullptr ,
0 ,
nullptr ,
Z_DEFLATED ,
Z_DEFAULT_COMPRESSION ,
0 ,
- MAX_WBITS ,
DEF_MEM_LEVEL ,
Z_DEFAULT_STRATEGY ,
nullptr ,
0 ,
0x0314 , // "version made by", 0x03 - Unix, 0x14 - ZIP specification version 2.0, required to store Unix file permissions
0 ) ;
Vector < uint8_t > array = FileAccess : : get_file_as_array ( dir . plus_file ( f ) ) ;
zipWriteInFileInZip ( p_zip , array . ptr ( ) , array . size ( ) ) ;
zipCloseFileInZip ( p_zip ) ;
}
}
da - > list_dir_end ( ) ;
memdelete ( da ) ;
}
2017-06-21 11:11:38 +02:00
bool EditorExportPlatformOSX : : can_export ( const Ref < EditorExportPreset > & p_preset , String & r_error , bool & r_missing_templates ) const {
2014-02-10 02:10:30 +01:00
String err ;
2020-01-07 13:29:02 +01:00
bool valid = false ;
2014-02-10 02:10:30 +01:00
2020-01-07 13:29:02 +01:00
// Look for export templates (first official, and if defined custom templates).
2014-02-10 02:10:30 +01:00
2020-01-07 13:29:02 +01:00
bool dvalid = exists_export_template ( " osx.zip " , & err ) ;
bool rvalid = dvalid ; // Both in the same ZIP.
if ( p_preset - > get ( " custom_template/debug " ) ! = " " ) {
dvalid = FileAccess : : exists ( p_preset - > get ( " custom_template/debug " ) ) ;
if ( ! dvalid ) {
2019-01-21 18:34:53 +01:00
err + = TTR ( " Custom debug template not found. " ) + " \n " ;
2018-03-03 14:23:00 +01:00
}
2014-02-10 02:10:30 +01:00
}
2020-01-07 13:29:02 +01:00
if ( p_preset - > get ( " custom_template/release " ) ! = " " ) {
rvalid = FileAccess : : exists ( p_preset - > get ( " custom_template/release " ) ) ;
if ( ! rvalid ) {
2019-01-21 18:34:53 +01:00
err + = TTR ( " Custom release template not found. " ) + " \n " ;
2018-03-03 14:23:00 +01:00
}
2014-02-10 02:10:30 +01:00
}
2020-01-07 13:29:02 +01:00
valid = dvalid | | rvalid ;
r_missing_templates = ! valid ;
2017-06-21 11:11:38 +02:00
if ( ! err . empty ( ) )
r_error = err ;
2014-02-10 02:10:30 +01:00
return valid ;
}
2017-06-21 11:11:38 +02:00
EditorExportPlatformOSX : : EditorExportPlatformOSX ( ) {
2014-02-10 02:10:30 +01:00
2017-06-21 11:11:38 +02:00
Ref < Image > img = memnew ( Image ( _osx_logo ) ) ;
logo . instance ( ) ;
logo - > create_from_image ( img ) ;
}
2014-02-10 02:10:30 +01:00
2017-06-21 11:11:38 +02:00
EditorExportPlatformOSX : : ~ EditorExportPlatformOSX ( ) {
2014-02-10 02:10:30 +01:00
}
void register_osx_exporter ( ) {
2017-06-21 11:11:38 +02:00
Ref < EditorExportPlatformOSX > platform ;
platform . instance ( ) ;
EditorExport : : get_singleton ( ) - > add_export_platform ( platform ) ;
2014-02-10 02:10:30 +01:00
}