Mono: Fix opening code editors in OSX and cleanup

This commit is contained in:
Ignacio Etcheverry 2018-09-17 16:40:26 +02:00
parent b032738a51
commit 50f6dbff87
13 changed files with 425 additions and 51 deletions

View File

@ -2,13 +2,23 @@ using System;
using System.IO;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
namespace GodotSharpTools.Editor
{
public class MonoDevelopInstance
{
private Process process;
private string solutionFile;
public enum EditorId
{
MonoDevelop = 0,
VisualStudioForMac = 1
}
readonly string solutionFile;
readonly EditorId editorId;
Process process;
public void Execute(string[] files)
{
@ -16,6 +26,35 @@ namespace GodotSharpTools.Editor
List<string> args = new List<string>();
string command;
if (Utils.OS.IsOSX())
{
string bundleId = codeEditorBundleIds[editorId];
if (IsApplicationBundleInstalled(bundleId))
{
command = "open";
args.Add("-b");
args.Add(bundleId);
// The 'open' process must wait until the application finishes
if (newWindow)
args.Add("--wait-apps");
args.Add("--args");
}
else
{
command = codeEditorPaths[editorId];
}
}
else
{
command = codeEditorPaths[editorId];
}
args.Add("--ipc-tcp");
if (newWindow)
@ -33,25 +72,73 @@ namespace GodotSharpTools.Editor
if (newWindow)
{
ProcessStartInfo startInfo = new ProcessStartInfo(MonoDevelopFile, string.Join(" ", args));
process = Process.Start(startInfo);
process = Process.Start(new ProcessStartInfo()
{
FileName = command,
Arguments = string.Join(" ", args),
UseShellExecute = false
});
}
else
{
Process.Start(MonoDevelopFile, string.Join(" ", args));
Process.Start(new ProcessStartInfo()
{
FileName = command,
Arguments = string.Join(" ", args),
UseShellExecute = false
});
}
}
public MonoDevelopInstance(string solutionFile)
public MonoDevelopInstance(string solutionFile, EditorId editorId)
{
if (editorId == EditorId.VisualStudioForMac && !Utils.OS.IsOSX())
throw new InvalidOperationException($"{nameof(EditorId.VisualStudioForMac)} not supported on this platform");
this.solutionFile = solutionFile;
this.editorId = editorId;
}
private static string MonoDevelopFile
[MethodImpl(MethodImplOptions.InternalCall)]
private extern static bool IsApplicationBundleInstalled(string bundleId);
static readonly IReadOnlyDictionary<EditorId, string> codeEditorPaths;
static readonly IReadOnlyDictionary<EditorId, string> codeEditorBundleIds;
static MonoDevelopInstance()
{
get
if (Utils.OS.IsOSX())
{
return "monodevelop";
codeEditorPaths = new Dictionary<EditorId, string>
{
// Rely on PATH
{ EditorId.MonoDevelop, "monodevelop" },
{ EditorId.VisualStudioForMac, "VisualStudio" }
};
codeEditorBundleIds = new Dictionary<EditorId, string>
{
// TODO EditorId.MonoDevelop
{ EditorId.VisualStudioForMac, "com.microsoft.visual-studio" }
};
}
else if (Utils.OS.IsWindows())
{
codeEditorPaths = new Dictionary<EditorId, string>
{
// XamarinStudio is no longer a thing, and the latest version is quite old
// MonoDevelop is available from source only on Windows. The recommendation
// is to use Visual Studio instead. Since there are no official builds, we
// will rely on custom MonoDevelop builds being added to PATH.
{ EditorId.MonoDevelop, "MonoDevelop.exe" }
};
}
else if (Utils.OS.IsUnix())
{
codeEditorPaths = new Dictionary<EditorId, string>
{
// Rely on PATH
{ EditorId.MonoDevelop, "monodevelop" }
};
}
}
}

View File

@ -40,6 +40,7 @@
<Compile Include="Project\ProjectGenerator.cs" />
<Compile Include="Project\ProjectUtils.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Utils\OS.cs" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -1,14 +0,0 @@
<Properties StartupItem="GodotSharpTools.csproj">
<MonoDevelop.Ide.Workspace ActiveConfiguration="Debug" />
<MonoDevelop.Ide.Workbench ActiveDocument="Build/BuildSystem.cs">
<Files>
<File FileName="Build/ProjectExtensions.cs" Line="1" Column="1" />
<File FileName="Build/ProjectGenerator.cs" Line="1" Column="1" />
<File FileName="Build/BuildSystem.cs" Line="37" Column="14" />
</Files>
</MonoDevelop.Ide.Workbench>
<MonoDevelop.Ide.DebuggingService.Breakpoints>
<BreakpointStore />
</MonoDevelop.Ide.DebuggingService.Breakpoints>
<MonoDevelop.Ide.DebuggingService.PinnedWatches />
</Properties>

View File

@ -0,0 +1,62 @@
using System;
using System.Linq;
using System.Runtime.CompilerServices;
namespace GodotSharpTools.Utils
{
public static class OS
{
[MethodImpl(MethodImplOptions.InternalCall)]
extern static string GetPlatformName();
const string HaikuName = "Haiku";
const string OSXName = "OSX";
const string ServerName = "Server";
const string UWPName = "UWP";
const string WindowsName = "Windows";
const string X11Name = "X11";
public static bool IsHaiku()
{
return HaikuName.Equals(GetPlatformName(), StringComparison.OrdinalIgnoreCase);
}
public static bool IsOSX()
{
return OSXName.Equals(GetPlatformName(), StringComparison.OrdinalIgnoreCase);
}
public static bool IsServer()
{
return ServerName.Equals(GetPlatformName(), StringComparison.OrdinalIgnoreCase);
}
public static bool IsUWP()
{
return UWPName.Equals(GetPlatformName(), StringComparison.OrdinalIgnoreCase);
}
public static bool IsWindows()
{
return WindowsName.Equals(GetPlatformName(), StringComparison.OrdinalIgnoreCase);
}
public static bool IsX11()
{
return X11Name.Equals(GetPlatformName(), StringComparison.OrdinalIgnoreCase);
}
static bool? IsUnixCache = null;
static readonly string[] UnixPlatforms = new string[] { HaikuName, OSXName, ServerName, X11Name };
public static bool IsUnix()
{
if (IsUnixCache.HasValue)
return IsUnixCache.Value;
string osName = GetPlatformName();
IsUnixCache = UnixPlatforms.Any(p => p.Equals(osName, StringComparison.OrdinalIgnoreCase));
return IsUnixCache.Value;
}
}
}

View File

@ -94,7 +94,12 @@ MonoString *godot_icall_BuildInstance_get_MSBuildPath() {
#if defined(WINDOWS_ENABLED)
switch (build_tool) {
case GodotSharpBuilds::MSBUILD_VS: {
static String msbuild_tools_path = MonoRegUtils::find_msbuild_tools_path();
static String msbuild_tools_path;
if (msbuild_tools_path.empty() || !FileAccess::exists(msbuild_tools_path)) {
// Try to search it again if it wasn't found last time or if it was removed from its location
msbuild_tools_path = MonoRegUtils::find_msbuild_tools_path();
}
if (msbuild_tools_path.length()) {
if (!msbuild_tools_path.ends_with("\\"))
@ -128,15 +133,25 @@ MonoString *godot_icall_BuildInstance_get_MSBuildPath() {
CRASH_NOW();
}
#elif defined(UNIX_ENABLED)
static String msbuild_path = _find_build_engine_on_unix("msbuild");
static String xbuild_path = _find_build_engine_on_unix("xbuild");
static String msbuild_path;
static String xbuild_path;
if (build_tool == GodotSharpBuilds::XBUILD) {
if (xbuild_path.empty() || !FileAccess::exists(xbuild_path)) {
// Try to search it again if it wasn't found last time or if it was removed from its location
xbuild_path = _find_build_engine_on_unix("msbuild");
}
if (xbuild_path.empty()) {
WARN_PRINT("Cannot find binary for '" PROP_NAME_XBUILD "'");
return NULL;
}
} else {
if (msbuild_path.empty() || !FileAccess::exists(msbuild_path)) {
// Try to search it again if it wasn't found last time or if it was removed from its location
msbuild_path = _find_build_engine_on_unix("msbuild");
}
if (msbuild_path.empty()) {
WARN_PRINT("Cannot find binary for '" PROP_NAME_MSBUILD_MONO "'");
return NULL;
@ -192,7 +207,11 @@ MonoBoolean godot_icall_BuildInstance_get_UsingMonoMSBuildOnWindows() {
#endif
}
void GodotSharpBuilds::_register_internal_calls() {
void GodotSharpBuilds::register_internal_calls() {
static bool registered = false;
ERR_FAIL_COND(registered);
registered = true;
mono_add_internal_call("GodotSharpTools.Build.BuildSystem::godot_icall_BuildInstance_ExitCallback", (void *)godot_icall_BuildInstance_ExitCallback);
mono_add_internal_call("GodotSharpTools.Build.BuildInstance::godot_icall_BuildInstance_get_MSBuildPath", (void *)godot_icall_BuildInstance_get_MSBuildPath);

View File

@ -61,9 +61,6 @@ private:
static GodotSharpBuilds *singleton;
friend class GDMono;
static void _register_internal_calls();
public:
enum BuildTool {
MSBUILD_MONO,
@ -75,6 +72,8 @@ public:
_FORCE_INLINE_ static GodotSharpBuilds *get_singleton() { return singleton; }
static void register_internal_calls();
static void show_build_error_dialog(const String &p_message);
void build_exit_callback(const MonoBuildInfo &p_build_info, int p_exit_code);

View File

@ -38,12 +38,17 @@
#include "../csharp_script.h"
#include "../godotsharp_dirs.h"
#include "../mono_gd/gd_mono.h"
#include "../mono_gd/gd_mono_marshal.h"
#include "../utils/path_utils.h"
#include "bindings_generator.h"
#include "csharp_project.h"
#include "godotsharp_export.h"
#include "net_solution.h"
#ifdef OSX_ENABLED
#include "../utils/osx_utils.h"
#endif
#ifdef WINDOWS_ENABLED
#include "../utils/mono_reg_utils.h"
#endif
@ -169,6 +174,26 @@ void GodotSharpEditor::_bind_methods() {
ClassDB::bind_method(D_METHOD("_menu_option_pressed", "id"), &GodotSharpEditor::_menu_option_pressed);
}
MonoBoolean godot_icall_MonoDevelopInstance_IsApplicationBundleInstalled(MonoString *p_bundle_id) {
#ifdef OSX_ENABLED
return (MonoBoolean)osx_is_app_bundle_installed(GDMonoMarshal::mono_string_to_godot(p_bundle_id));
#else
(void)p_bundle_id; // UNUSED
ERR_FAIL_V(false);
#endif
}
void GodotSharpEditor::register_internal_calls() {
static bool registered = false;
ERR_FAIL_COND(registered);
registered = true;
mono_add_internal_call("GodotSharpTools.Editor.MonoDevelopInstance::IsApplicationBundleInstalled", (void *)godot_icall_MonoDevelopInstance_IsApplicationBundleInstalled);
GodotSharpBuilds::register_internal_calls();
}
void GodotSharpEditor::show_error_dialog(const String &p_message, const String &p_title) {
error_dialog->set_title(p_title);
@ -181,8 +206,36 @@ Error GodotSharpEditor::open_in_external_editor(const Ref<Script> &p_script, int
ExternalEditor editor = ExternalEditor(int(EditorSettings::get_singleton()->get("mono/editor/external_editor")));
switch (editor) {
case EDITOR_CODE: {
case EDITOR_VSCODE: {
static String vscode_path;
if (vscode_path.empty() || !FileAccess::exists(vscode_path)) {
// Try to search it again if it wasn't found last time or if it was removed from its location
vscode_path = path_which("code");
}
List<String> args;
#ifdef OSX_ENABLED
// The package path is '/Applications/Visual Studio Code.app'
static const String vscode_bundle_id = "com.microsoft.VSCode";
static bool osx_app_bundle_installed = osx_is_app_bundle_installed(vscode_bundle_id);
if (osx_app_bundle_installed) {
args.push_back("-b");
args.push_back(vscode_bundle_id);
// The reusing of existing windows made by the 'open' command might not choose a wubdiw that is
// editing our folder. It's better to ask for a new window and let VSCode do the window management.
args.push_back("-n");
// The open process must wait until the application finishes (which is instant in VSCode's case)
args.push_back("--wait-apps");
args.push_back("--args");
}
#endif
args.push_back(ProjectSettings::get_singleton()->get_resource_path());
String script_path = ProjectSettings::get_singleton()->globalize_path(p_script->get_path());
@ -194,18 +247,47 @@ Error GodotSharpEditor::open_in_external_editor(const Ref<Script> &p_script, int
args.push_back(script_path);
}
static String program = path_which("code");
#ifdef OSX_ENABLED
ERR_EXPLAIN("Cannot find code editor: VSCode");
ERR_FAIL_COND_V(!osx_app_bundle_installed && vscode_path.empty(), ERR_FILE_NOT_FOUND);
Error err = OS::get_singleton()->execute(program.length() ? program : "code", args, false);
String command = osx_app_bundle_installed ? "/usr/bin/open" : vscode_path;
#else
ERR_EXPLAIN("Cannot find code editor: VSCode");
ERR_FAIL_COND_V(vscode_path.empty(), ERR_FILE_NOT_FOUND);
String command = vscode_path;
#endif
Error err = OS::get_singleton()->execute(command, args, false);
if (err != OK) {
ERR_PRINT("GodotSharp: Could not execute external editor");
ERR_PRINT("Error when trying to execute code editor: VSCode");
return err;
}
} break;
#ifdef OSX_ENABLED
case EDITOR_VISUALSTUDIO_MAC:
// [[fallthrough]];
#endif
case EDITOR_MONODEVELOP: {
if (!monodevel_instance)
monodevel_instance = memnew(MonoDevelopInstance(GodotSharpDirs::get_project_sln_path()));
#ifdef OSX_ENABLED
bool is_visualstudio = editor == EDITOR_VISUALSTUDIO_MAC;
MonoDevelopInstance **instance = is_visualstudio ?
&visualstudio_mac_instance :
&monodevelop_instance;
MonoDevelopInstance::EditorId editor_id = is_visualstudio ?
MonoDevelopInstance::VISUALSTUDIO_FOR_MAC :
MonoDevelopInstance::MONODEVELOP;
#else
MonoDevelopInstance **instance = &monodevelop_instance;
MonoDevelopInstance::EditorId editor_id = MonoDevelopInstance::MONODEVELOP;
#endif
if (!*instance)
*instance = memnew(MonoDevelopInstance(GodotSharpDirs::get_project_sln_path(), editor_id));
String script_path = ProjectSettings::get_singleton()->globalize_path(p_script->get_path());
@ -213,7 +295,7 @@ Error GodotSharpEditor::open_in_external_editor(const Ref<Script> &p_script, int
script_path += ";" + itos(p_line + 1) + ";" + itos(p_col);
}
monodevel_instance->execute(script_path);
(*instance)->execute(script_path);
} break;
default:
return ERR_UNAVAILABLE;
@ -231,7 +313,10 @@ GodotSharpEditor::GodotSharpEditor(EditorNode *p_editor) {
singleton = this;
monodevel_instance = NULL;
monodevelop_instance = NULL;
#ifdef OSX_ENABLED
visualstudio_mac_instance = NULL;
#endif
editor = p_editor;
@ -314,7 +399,18 @@ GodotSharpEditor::GodotSharpEditor(EditorNode *p_editor) {
// External editor settings
EditorSettings *ed_settings = EditorSettings::get_singleton();
EDITOR_DEF("mono/editor/external_editor", EDITOR_NONE);
ed_settings->add_property_hint(PropertyInfo(Variant::INT, "mono/editor/external_editor", PROPERTY_HINT_ENUM, "None,MonoDevelop,Visual Studio Code"));
String settings_hint_str = "None";
#ifdef WINDOWS_ENABLED
settings_hint_str += ",MonoDevelop,Visual Studio Code";
#elif OSX_ENABLED
settings_hint_str += ",Visual Studio,MonoDevelop,Visual Studio Code";
#elif UNIX_ENABLED
settings_hint_str += ",MonoDevelop,Visual Studio Code";
#endif
ed_settings->add_property_hint(PropertyInfo(Variant::INT, "mono/editor/external_editor", PROPERTY_HINT_ENUM, settings_hint_str));
// Export plugin
Ref<GodotSharpExport> godotsharp_export;
@ -328,9 +424,9 @@ GodotSharpEditor::~GodotSharpEditor() {
memdelete(godotsharp_builds);
if (monodevel_instance) {
memdelete(monodevel_instance);
monodevel_instance = NULL;
if (monodevelop_instance) {
memdelete(monodevelop_instance);
monodevelop_instance = NULL;
}
}

View File

@ -50,7 +50,10 @@ class GodotSharpEditor : public Node {
GodotSharpBuilds *godotsharp_builds;
MonoDevelopInstance *monodevel_instance;
MonoDevelopInstance *monodevelop_instance;
#ifdef OSX_ENABLED
MonoDevelopInstance *visualstudio_mac_instance;
#endif
bool _create_project_solution();
@ -74,12 +77,24 @@ public:
enum ExternalEditor {
EDITOR_NONE,
#ifdef WINDOWS_ENABLED
//EDITOR_VISUALSTUDIO, // TODO
EDITOR_MONODEVELOP,
EDITOR_CODE,
EDITOR_VSCODE
#elif OSX_ENABLED
EDITOR_VISUALSTUDIO_MAC,
EDITOR_MONODEVELOP,
EDITOR_VSCODE
#elif UNIX_ENABLED
EDITOR_MONODEVELOP,
EDITOR_VSCODE
#endif
};
_FORCE_INLINE_ static GodotSharpEditor *get_singleton() { return singleton; }
static void register_internal_calls();
void show_error_dialog(const String &p_message, const String &p_title = "Error");
Error open_in_external_editor(const Ref<Script> &p_script, int p_line, int p_col);

View File

@ -47,7 +47,7 @@ void MonoDevelopInstance::execute(const Vector<String> &p_files) {
execute_method->invoke(gc_handle->get_target(), args, &exc);
if (exc) {
GDMonoUtils::debug_unhandled_exception(exc);
GDMonoUtils::debug_print_unhandled_exception(exc);
ERR_FAIL();
}
}
@ -59,7 +59,7 @@ void MonoDevelopInstance::execute(const String &p_file) {
execute(files);
}
MonoDevelopInstance::MonoDevelopInstance(const String &p_solution) {
MonoDevelopInstance::MonoDevelopInstance(const String &p_solution, EditorId p_editor_id) {
_GDMONO_SCOPE_DOMAIN_(TOOLS_DOMAIN)
@ -67,15 +67,16 @@ MonoDevelopInstance::MonoDevelopInstance(const String &p_solution) {
MonoObject *obj = mono_object_new(TOOLS_DOMAIN, klass->get_mono_ptr());
GDMonoMethod *ctor = klass->get_method(".ctor", 1);
GDMonoMethod *ctor = klass->get_method(".ctor", 2);
MonoException *exc = NULL;
Variant solution = p_solution;
const Variant *args[1] = { &solution };
Variant editor_id = p_editor_id;
const Variant *args[2] = { &solution, &editor_id };
ctor->invoke(obj, args, &exc);
if (exc) {
GDMonoUtils::debug_unhandled_exception(exc);
GDMonoUtils::debug_print_unhandled_exception(exc);
ERR_FAIL();
}

View File

@ -42,10 +42,15 @@ class MonoDevelopInstance {
GDMonoMethod *execute_method;
public:
enum EditorId {
MONODEVELOP = 0,
VISUALSTUDIO_FOR_MAC = 1
};
void execute(const Vector<String> &p_files);
void execute(const String &p_file);
MonoDevelopInstance(const String &p_solution);
MonoDevelopInstance(const String &p_solution, EditorId p_editor_id);
};
#endif // MONODEVELOP_INSTANCE_H

View File

@ -319,7 +319,7 @@ void GDMono::_register_internal_calls() {
#endif
#ifdef TOOLS_ENABLED
GodotSharpBuilds::_register_internal_calls();
GodotSharpEditor::register_internal_calls();
#endif
}

View File

@ -0,0 +1,62 @@
/*************************************************************************/
/* osx_utils.cpp */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2018 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2018 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* 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. */
/*************************************************************************/
#include "osx_utils.h"
#include "core/print_string.h"
#ifdef OSX_ENABLED
#include <CoreFoundation/CoreFoundation.h>
#include <CoreServices/CoreServices.h>
bool osx_is_app_bundle_installed(const String &p_bundle_id) {
CFURLRef app_url = NULL;
CFStringRef bundle_id = CFStringCreateWithCString(NULL, p_bundle_id.utf8(), kCFStringEncodingUTF8);
OSStatus result = LSFindApplicationForInfo(kLSUnknownCreator, bundle_id, NULL, NULL, &app_url);
CFRelease(bundle_id);
if (app_url)
CFRelease(app_url);
switch (result) {
case noErr:
return true;
case kLSApplicationNotFoundErr:
break;
default:
break;
}
return false;
}
#endif

View File

@ -0,0 +1,41 @@
/*************************************************************************/
/* osx_utils.h */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2018 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2018 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* 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. */
/*************************************************************************/
#include "core/ustring.h"
#ifndef OSX_UTILS_H
#ifdef OSX_ENABLED
bool osx_is_app_bundle_installed(const String &p_bundle_id);
#endif
#endif // OSX_UTILS_H