mirror of
https://github.com/Relintai/mono.git
synced 2024-11-08 10:12:16 +01:00
Added godot's mono module.
This commit is contained in:
commit
5c461069b3
14
.editorconfig
Normal file
14
.editorconfig
Normal file
@ -0,0 +1,14 @@
|
||||
[*.sln]
|
||||
indent_style = tab
|
||||
|
||||
[*.{csproj,props,targets,nuspec,resx}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.cs]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
max_line_length = 120
|
||||
csharp_indent_case_contents_when_block = false
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# Do not ignore solution files inside the mono module. Overrides Godot's global gitignore.
|
||||
!*.sln
|
21
LICENSE.txt
Normal file
21
LICENSE.txt
Normal file
@ -0,0 +1,21 @@
|
||||
Copyright (c) 2023-present Péter Magyar.
|
||||
Copyright (c) 2014-2023 Godot Engine contributors (cf. AUTHORS.md).
|
||||
Copyright (c) 2007-2023 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.
|
64
SCsub
Normal file
64
SCsub
Normal file
@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import build_scripts.tls_configure as tls_configure
|
||||
import build_scripts.mono_configure as mono_configure
|
||||
|
||||
Import("env")
|
||||
Import("env_modules")
|
||||
|
||||
env_mono = env_modules.Clone()
|
||||
|
||||
if env_mono["tools"]:
|
||||
# NOTE: It is safe to generate this file here, since this is still executed serially
|
||||
import build_scripts.gen_cs_glue_version as gen_cs_glue_version
|
||||
|
||||
gen_cs_glue_version.generate_header("glue/GodotSharp", "glue/cs_glue_version.gen.h")
|
||||
|
||||
# Glue sources
|
||||
if env_mono["mono_glue"]:
|
||||
env_mono.Append(CPPDEFINES=["MONO_GLUE_ENABLED"])
|
||||
|
||||
import os.path
|
||||
|
||||
if not os.path.isfile("glue/mono_glue.gen.cpp"):
|
||||
raise RuntimeError("Mono glue sources not found. Did you forget to run '--generate-mono-glue'?")
|
||||
|
||||
# Configure Thread Local Storage
|
||||
|
||||
conf = Configure(env_mono)
|
||||
tls_configure.configure(conf)
|
||||
env_mono = conf.Finish()
|
||||
|
||||
# Configure Mono
|
||||
|
||||
mono_configure.configure(env, env_mono)
|
||||
|
||||
if env_mono["tools"] and env_mono["mono_glue"] and env_mono["build_cil"]:
|
||||
# Build Godot API solution
|
||||
import build_scripts.api_solution_build as api_solution_build
|
||||
|
||||
api_sln_cmd = api_solution_build.build(env_mono)
|
||||
|
||||
# Build GodotTools
|
||||
import build_scripts.godot_tools_build as godot_tools_build
|
||||
|
||||
godot_tools_build.build(env_mono, api_sln_cmd)
|
||||
|
||||
# Add sources
|
||||
|
||||
env_mono.add_source_files(env.modules_sources, "*.cpp")
|
||||
env_mono.add_source_files(env.modules_sources, "glue/*.cpp")
|
||||
env_mono.add_source_files(env.modules_sources, "glue/mono_glue.gen.cpp")
|
||||
env_mono.add_source_files(env.modules_sources, "mono_gd/*.cpp")
|
||||
env_mono.add_source_files(env.modules_sources, "utils/*.cpp")
|
||||
|
||||
env_mono.add_source_files(env.modules_sources, "mono_gd/support/*.cpp")
|
||||
|
||||
if env["platform"] in ["osx", "iphone"]:
|
||||
env_mono.add_source_files(env.modules_sources, "mono_gd/support/*.mm")
|
||||
env_mono.add_source_files(env.modules_sources, "mono_gd/support/*.m")
|
||||
elif env["platform"] == "android":
|
||||
env_mono.add_source_files(env.modules_sources, "mono_gd/android_mono_config.gen.cpp")
|
||||
|
||||
if env["tools"]:
|
||||
env_mono.add_source_files(env.modules_sources, "editor/*.cpp")
|
0
__init__.py
Normal file
0
__init__.py
Normal file
0
build_scripts/__init__.py
Normal file
0
build_scripts/__init__.py
Normal file
80
build_scripts/api_solution_build.py
Normal file
80
build_scripts/api_solution_build.py
Normal file
@ -0,0 +1,80 @@
|
||||
# Build the Godot API solution
|
||||
|
||||
import os
|
||||
|
||||
from SCons.Script import Dir
|
||||
|
||||
|
||||
def build_api_solution(source, target, env):
|
||||
# source and target elements are of type SCons.Node.FS.File, hence why we convert them to str
|
||||
|
||||
module_dir = env["module_dir"]
|
||||
|
||||
solution_path = os.path.join(module_dir, "glue/GodotSharp/GodotSharp.sln")
|
||||
|
||||
build_config = env["solution_build_config"]
|
||||
|
||||
extra_msbuild_args = ["/p:NoWarn=1591"] # Ignore missing documentation warnings
|
||||
|
||||
from .solution_builder import build_solution
|
||||
|
||||
build_solution(env, solution_path, build_config, extra_msbuild_args=extra_msbuild_args)
|
||||
|
||||
# Copy targets
|
||||
|
||||
core_src_dir = os.path.abspath(os.path.join(solution_path, os.pardir, "GodotSharp", "bin", build_config))
|
||||
editor_src_dir = os.path.abspath(os.path.join(solution_path, os.pardir, "GodotSharpEditor", "bin", build_config))
|
||||
|
||||
dst_dir = os.path.abspath(os.path.join(str(target[0]), os.pardir))
|
||||
|
||||
if not os.path.isdir(dst_dir):
|
||||
assert not os.path.isfile(dst_dir)
|
||||
os.makedirs(dst_dir)
|
||||
|
||||
def copy_target(target_path):
|
||||
from shutil import copy
|
||||
|
||||
filename = os.path.basename(target_path)
|
||||
|
||||
src_path = os.path.join(core_src_dir, filename)
|
||||
if not os.path.isfile(src_path):
|
||||
src_path = os.path.join(editor_src_dir, filename)
|
||||
|
||||
copy(src_path, target_path)
|
||||
|
||||
for scons_target in target:
|
||||
copy_target(str(scons_target))
|
||||
|
||||
|
||||
def build(env_mono):
|
||||
assert env_mono["tools"]
|
||||
|
||||
target_filenames = [
|
||||
"GodotSharp.dll",
|
||||
"GodotSharp.pdb",
|
||||
"GodotSharp.xml",
|
||||
"GodotSharpEditor.dll",
|
||||
"GodotSharpEditor.pdb",
|
||||
"GodotSharpEditor.xml",
|
||||
]
|
||||
|
||||
depend_cmd = []
|
||||
|
||||
for build_config in ["Debug", "Release"]:
|
||||
output_dir = Dir("#bin").abspath
|
||||
editor_api_dir = os.path.join(output_dir, "GodotSharp", "Api", build_config)
|
||||
|
||||
targets = [os.path.join(editor_api_dir, filename) for filename in target_filenames]
|
||||
|
||||
cmd = env_mono.CommandNoCache(
|
||||
targets, depend_cmd, build_api_solution, module_dir=os.getcwd(), solution_build_config=build_config
|
||||
)
|
||||
env_mono.AlwaysBuild(cmd)
|
||||
|
||||
# Make the Release build of the API solution depend on the Debug build.
|
||||
# We do this in order to prevent SCons from building them in parallel,
|
||||
# which can freak out MSBuild. In many cases, one of the builds would
|
||||
# hang indefinitely requiring a key to be pressed for it to continue.
|
||||
depend_cmd = cmd
|
||||
|
||||
return depend_cmd
|
20
build_scripts/gen_cs_glue_version.py
Normal file
20
build_scripts/gen_cs_glue_version.py
Normal file
@ -0,0 +1,20 @@
|
||||
def generate_header(solution_dir, version_header_dst):
|
||||
import os
|
||||
|
||||
latest_mtime = 0
|
||||
for root, dirs, files in os.walk(solution_dir, topdown=True):
|
||||
dirs[:] = [d for d in dirs if d not in ["Generated"]] # Ignored generated files
|
||||
files = [f for f in files if f.endswith(".cs")]
|
||||
for file in files:
|
||||
filepath = os.path.join(root, file)
|
||||
mtime = os.path.getmtime(filepath)
|
||||
latest_mtime = mtime if mtime > latest_mtime else latest_mtime
|
||||
|
||||
glue_version = int(latest_mtime) # The latest modified time will do for now
|
||||
|
||||
with open(version_header_dst, "w") as version_header:
|
||||
version_header.write("/* THIS FILE IS GENERATED DO NOT EDIT */\n")
|
||||
version_header.write("#ifndef CS_GLUE_VERSION_H\n")
|
||||
version_header.write("#define CS_GLUE_VERSION_H\n\n")
|
||||
version_header.write("#define CS_GLUE_VERSION UINT32_C(" + str(glue_version) + ")\n")
|
||||
version_header.write("\n#endif // CS_GLUE_VERSION_H\n")
|
38
build_scripts/godot_tools_build.py
Normal file
38
build_scripts/godot_tools_build.py
Normal file
@ -0,0 +1,38 @@
|
||||
# Build GodotTools solution
|
||||
|
||||
import os
|
||||
|
||||
from SCons.Script import Dir
|
||||
|
||||
|
||||
def build_godot_tools(source, target, env):
|
||||
# source and target elements are of type SCons.Node.FS.File, hence why we convert them to str
|
||||
|
||||
module_dir = env["module_dir"]
|
||||
|
||||
solution_path = os.path.join(module_dir, "editor/GodotTools/GodotTools.sln")
|
||||
build_config = "Debug" if env["target"] == "debug" else "Release"
|
||||
|
||||
from .solution_builder import build_solution
|
||||
|
||||
extra_msbuild_args = ["/p:GodotPlatform=" + env["platform"]]
|
||||
|
||||
build_solution(env, solution_path, build_config, extra_msbuild_args)
|
||||
# No need to copy targets. The GodotTools csproj takes care of copying them.
|
||||
|
||||
|
||||
def build(env_mono, api_sln_cmd):
|
||||
assert env_mono["tools"]
|
||||
|
||||
output_dir = Dir("#bin").abspath
|
||||
editor_tools_dir = os.path.join(output_dir, "GodotSharp", "Tools")
|
||||
|
||||
target_filenames = ["GodotTools.dll"]
|
||||
|
||||
if env_mono["target"] == "debug":
|
||||
target_filenames += ["GodotTools.pdb"]
|
||||
|
||||
targets = [os.path.join(editor_tools_dir, filename) for filename in target_filenames]
|
||||
|
||||
cmd = env_mono.CommandNoCache(targets, api_sln_cmd, build_godot_tools, module_dir=os.getcwd())
|
||||
env_mono.AlwaysBuild(cmd)
|
55
build_scripts/make_android_mono_config.py
Normal file
55
build_scripts/make_android_mono_config.py
Normal file
@ -0,0 +1,55 @@
|
||||
def generate_compressed_config(config_src, output_dir):
|
||||
import os.path
|
||||
from compat import byte_to_str
|
||||
|
||||
# Source file
|
||||
with open(os.path.join(output_dir, "android_mono_config.gen.cpp"), "w") as cpp:
|
||||
with open(config_src, "rb") as f:
|
||||
buf = f.read()
|
||||
decompr_size = len(buf)
|
||||
import zlib
|
||||
|
||||
buf = zlib.compress(buf)
|
||||
compr_size = len(buf)
|
||||
|
||||
bytes_seq_str = ""
|
||||
for i, buf_idx in enumerate(range(compr_size)):
|
||||
if i > 0:
|
||||
bytes_seq_str += ", "
|
||||
bytes_seq_str += byte_to_str(buf[buf_idx])
|
||||
|
||||
cpp.write(
|
||||
"""/* THIS FILE IS GENERATED DO NOT EDIT */
|
||||
#include "android_mono_config.h"
|
||||
|
||||
#ifdef ANDROID_ENABLED
|
||||
|
||||
#include "core/io/compression.h"
|
||||
#include "core/pool_vector.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// config
|
||||
static const int config_compressed_size = %d;
|
||||
static const int config_uncompressed_size = %d;
|
||||
static const unsigned char config_compressed_data[] = { %s };
|
||||
|
||||
} // namespace
|
||||
|
||||
String get_godot_android_mono_config() {
|
||||
PoolVector<uint8_t> data;
|
||||
data.resize(config_uncompressed_size);
|
||||
PoolVector<uint8_t>::Write w = data.write();
|
||||
Compression::decompress(w.ptr(), config_uncompressed_size, config_compressed_data,
|
||||
config_compressed_size, Compression::MODE_DEFLATE);
|
||||
String s;
|
||||
if (s.parse_utf8((const char *)w.ptr(), data.size())) {
|
||||
ERR_FAIL_V(String());
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
#endif // ANDROID_ENABLED
|
||||
"""
|
||||
% (compr_size, decompr_size, bytes_seq_str)
|
||||
)
|
28
build_scripts/mono_android_config.xml
Normal file
28
build_scripts/mono_android_config.xml
Normal file
@ -0,0 +1,28 @@
|
||||
<configuration>
|
||||
<dllmap wordsize="32" dll="i:cygwin1.dll" target="/system/lib/libc.so" />
|
||||
<dllmap wordsize="64" dll="i:cygwin1.dll" target="/system/lib64/libc.so" />
|
||||
<dllmap wordsize="32" dll="libc" target="/system/lib/libc.so" />
|
||||
<dllmap wordsize="64" dll="libc" target="/system/lib64/libc.so" />
|
||||
<dllmap wordsize="32" dll="intl" target="/system/lib/libc.so" />
|
||||
<dllmap wordsize="64" dll="intl" target="/system/lib64/libc.so" />
|
||||
<dllmap wordsize="32" dll="libintl" target="/system/lib/libc.so" />
|
||||
<dllmap wordsize="64" dll="libintl" target="/system/lib64/libc.so" />
|
||||
<dllmap dll="MonoPosixHelper" target="libMonoPosixHelper.so" />
|
||||
<dllmap dll="System.Native" target="libmono-native.so" />
|
||||
<dllmap wordsize="32" dll="i:msvcrt" target="/system/lib/libc.so" />
|
||||
<dllmap wordsize="64" dll="i:msvcrt" target="/system/lib64/libc.so" />
|
||||
<dllmap wordsize="32" dll="i:msvcrt.dll" target="/system/lib/libc.so" />
|
||||
<dllmap wordsize="64" dll="i:msvcrt.dll" target="/system/lib64/libc.so" />
|
||||
<dllmap wordsize="32" dll="sqlite" target="/system/lib/libsqlite.so" />
|
||||
<dllmap wordsize="64" dll="sqlite" target="/system/lib64/libsqlite.so" />
|
||||
<dllmap wordsize="32" dll="sqlite3" target="/system/lib/libsqlite.so" />
|
||||
<dllmap wordsize="64" dll="sqlite3" target="/system/lib64/libsqlite.so" />
|
||||
<dllmap wordsize="32" dll="liblog" target="/system/lib/liblog.so" />
|
||||
<dllmap wordsize="64" dll="liblog" target="/system/lib64/liblog.so" />
|
||||
<dllmap dll="i:kernel32.dll">
|
||||
<dllentry dll="__Internal" name="CopyMemory" target="mono_win32_compat_CopyMemory"/>
|
||||
<dllentry dll="__Internal" name="FillMemory" target="mono_win32_compat_FillMemory"/>
|
||||
<dllentry dll="__Internal" name="MoveMemory" target="mono_win32_compat_MoveMemory"/>
|
||||
<dllentry dll="__Internal" name="ZeroMemory" target="mono_win32_compat_ZeroMemory"/>
|
||||
</dllmap>
|
||||
</configuration>
|
585
build_scripts/mono_configure.py
Normal file
585
build_scripts/mono_configure.py
Normal file
@ -0,0 +1,585 @@
|
||||
import os
|
||||
import os.path
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
from SCons.Script import Dir, Environment
|
||||
|
||||
if os.name == "nt":
|
||||
from . import mono_reg_utils as monoreg
|
||||
|
||||
|
||||
android_arch_dirs = {"armv7": "armeabi-v7a", "arm64v8": "arm64-v8a", "x86": "x86", "x86_64": "x86_64"}
|
||||
|
||||
|
||||
def get_android_out_dir(env):
|
||||
return os.path.join(
|
||||
Dir("#platform/android/java/lib/libs").abspath,
|
||||
"release" if env["target"] == "release" else "debug",
|
||||
android_arch_dirs[env["android_arch"]],
|
||||
)
|
||||
|
||||
|
||||
def find_name_in_dir_files(directory, names, prefixes=[""], extensions=[""]):
|
||||
for extension in extensions:
|
||||
if extension and not extension.startswith("."):
|
||||
extension = "." + extension
|
||||
for prefix in prefixes:
|
||||
for curname in names:
|
||||
if os.path.isfile(os.path.join(directory, prefix + curname + extension)):
|
||||
return curname
|
||||
return ""
|
||||
|
||||
|
||||
def find_file_in_dir(directory, names, prefixes=[""], extensions=[""]):
|
||||
for extension in extensions:
|
||||
if extension and not extension.startswith("."):
|
||||
extension = "." + extension
|
||||
for prefix in prefixes:
|
||||
for curname in names:
|
||||
filename = prefix + curname + extension
|
||||
if os.path.isfile(os.path.join(directory, filename)):
|
||||
return filename
|
||||
return ""
|
||||
|
||||
|
||||
def copy_file(src_dir, dst_dir, src_name, dst_name=""):
|
||||
from shutil import copy
|
||||
|
||||
src_path = os.path.join(Dir(src_dir).abspath, src_name)
|
||||
dst_dir = Dir(dst_dir).abspath
|
||||
|
||||
if not os.path.isdir(dst_dir):
|
||||
os.makedirs(dst_dir)
|
||||
|
||||
if dst_name:
|
||||
copy(src_path, os.path.join(dst_dir, dst_name))
|
||||
else:
|
||||
copy(src_path, dst_dir)
|
||||
|
||||
|
||||
def is_desktop(platform):
|
||||
return platform in ["windows", "osx", "x11", "server", "uwp", "haiku"]
|
||||
|
||||
|
||||
def is_unix_like(platform):
|
||||
return platform in ["osx", "x11", "server", "android", "haiku", "iphone"]
|
||||
|
||||
|
||||
def module_supports_tools_on(platform):
|
||||
return platform not in ["android", "javascript", "iphone"]
|
||||
|
||||
|
||||
def find_wasm_src_dir(mono_root):
|
||||
hint_dirs = [
|
||||
os.path.join(mono_root, "src"),
|
||||
os.path.join(mono_root, "../src"),
|
||||
]
|
||||
for hint_dir in hint_dirs:
|
||||
if os.path.isfile(os.path.join(hint_dir, "driver.c")):
|
||||
return hint_dir
|
||||
return ""
|
||||
|
||||
|
||||
def configure(env, env_mono):
|
||||
bits = env["bits"]
|
||||
is_android = env["platform"] == "android"
|
||||
is_javascript = env["platform"] == "javascript"
|
||||
is_ios = env["platform"] == "iphone"
|
||||
is_ios_sim = is_ios and env["ios_simulator"]
|
||||
|
||||
tools_enabled = env["tools"]
|
||||
mono_static = env["mono_static"]
|
||||
copy_mono_root = env["copy_mono_root"]
|
||||
|
||||
mono_prefix = env["mono_prefix"]
|
||||
mono_bcl = env["mono_bcl"]
|
||||
|
||||
mono_lib_names = ["mono-2.0-sgen", "monosgen-2.0"]
|
||||
|
||||
if is_android and not env["android_arch"] in android_arch_dirs:
|
||||
raise RuntimeError("This module does not support the specified 'android_arch': " + env["android_arch"])
|
||||
|
||||
if tools_enabled and not module_supports_tools_on(env["platform"]):
|
||||
# TODO:
|
||||
# Android: We have to add the data directory to the apk, concretely the Api and Tools folders.
|
||||
raise RuntimeError("This module does not currently support building for this platform with tools enabled")
|
||||
|
||||
if is_android and mono_static:
|
||||
# FIXME: When static linking and doing something that requires libmono-native, we get a dlopen error as 'libmono-native'
|
||||
# seems to depend on 'libmonosgen-2.0'. Could be fixed by re-directing to '__Internal' with a dllmap or in the dlopen hook.
|
||||
raise RuntimeError("Statically linking Mono is not currently supported for this platform")
|
||||
|
||||
if not mono_static and (is_javascript or is_ios):
|
||||
raise RuntimeError("Dynamically linking Mono is not currently supported for this platform")
|
||||
|
||||
if not mono_prefix and (os.getenv("MONO32_PREFIX") or os.getenv("MONO64_PREFIX")):
|
||||
print(
|
||||
"WARNING: The environment variables 'MONO32_PREFIX' and 'MONO64_PREFIX' are deprecated; use the 'mono_prefix' SCons parameter instead"
|
||||
)
|
||||
|
||||
# Although we don't support building with tools for any platform where we currently use static AOT,
|
||||
# if these are supported in the future, we won't be using static AOT for them as that would be
|
||||
# too restrictive for the editor. These builds would probably be made to only use the interpreter.
|
||||
mono_aot_static = (is_ios and not is_ios_sim) and not env["tools"]
|
||||
|
||||
# Static AOT is only supported on the root domain
|
||||
mono_single_appdomain = mono_aot_static
|
||||
|
||||
if mono_single_appdomain:
|
||||
env_mono.Append(CPPDEFINES=["GD_MONO_SINGLE_APPDOMAIN"])
|
||||
|
||||
if (env["tools"] or env["target"] != "release") and not mono_single_appdomain:
|
||||
env_mono.Append(CPPDEFINES=["GD_MONO_HOT_RELOAD"])
|
||||
|
||||
if env["platform"] == "windows":
|
||||
mono_root = mono_prefix
|
||||
|
||||
if not mono_root and os.name == "nt":
|
||||
mono_root = monoreg.find_mono_root_dir(bits)
|
||||
|
||||
if not mono_root:
|
||||
raise RuntimeError(
|
||||
"Mono installation directory not found; specify one manually with the 'mono_prefix' SCons parameter"
|
||||
)
|
||||
|
||||
print("Found Mono root directory: " + mono_root)
|
||||
|
||||
mono_lib_path = os.path.join(mono_root, "lib")
|
||||
|
||||
env.Append(LIBPATH=mono_lib_path)
|
||||
env_mono.Prepend(CPPPATH=os.path.join(mono_root, "include", "mono-2.0"))
|
||||
|
||||
lib_suffixes = [".lib"]
|
||||
|
||||
if not env.msvc:
|
||||
# MingW supports both '.a' and '.lib'
|
||||
lib_suffixes.insert(0, ".a")
|
||||
|
||||
if mono_static:
|
||||
if env.msvc:
|
||||
mono_static_lib_name = "libmono-static-sgen"
|
||||
else:
|
||||
mono_static_lib_name = "libmonosgen-2.0"
|
||||
|
||||
mono_static_lib_file = find_file_in_dir(mono_lib_path, [mono_static_lib_name], extensions=lib_suffixes)
|
||||
|
||||
if not mono_static_lib_file:
|
||||
raise RuntimeError("Could not find static mono library in: " + mono_lib_path)
|
||||
|
||||
if env.msvc:
|
||||
env.Append(LINKFLAGS=mono_static_lib_file)
|
||||
|
||||
env.Append(LINKFLAGS="Mincore.lib")
|
||||
env.Append(LINKFLAGS="msvcrt.lib")
|
||||
env.Append(LINKFLAGS="LIBCMT.lib")
|
||||
env.Append(LINKFLAGS="Psapi.lib")
|
||||
else:
|
||||
mono_static_lib_file_path = os.path.join(mono_lib_path, mono_static_lib_file)
|
||||
env.Append(LINKFLAGS=["-Wl,-whole-archive", mono_static_lib_file_path, "-Wl,-no-whole-archive"])
|
||||
|
||||
env.Append(LIBS=["psapi"])
|
||||
env.Append(LIBS=["version"])
|
||||
else:
|
||||
mono_lib_name = find_name_in_dir_files(
|
||||
mono_lib_path, mono_lib_names, prefixes=["", "lib"], extensions=lib_suffixes
|
||||
)
|
||||
|
||||
if not mono_lib_name:
|
||||
raise RuntimeError("Could not find mono library in: " + mono_lib_path)
|
||||
|
||||
if env.msvc:
|
||||
env.Append(LINKFLAGS=mono_lib_name + ".lib")
|
||||
else:
|
||||
env.Append(LIBS=[mono_lib_name])
|
||||
|
||||
mono_bin_path = os.path.join(mono_root, "bin")
|
||||
|
||||
mono_dll_file = find_file_in_dir(mono_bin_path, mono_lib_names, prefixes=["", "lib"], extensions=[".dll"])
|
||||
|
||||
if not mono_dll_file:
|
||||
raise RuntimeError("Could not find mono shared library in: " + mono_bin_path)
|
||||
|
||||
copy_file(mono_bin_path, "#bin", mono_dll_file)
|
||||
else:
|
||||
is_apple = env["platform"] in ["osx", "iphone"]
|
||||
is_macos = is_apple and not is_ios
|
||||
|
||||
sharedlib_ext = ".dylib" if is_apple else ".so"
|
||||
|
||||
mono_root = mono_prefix
|
||||
mono_lib_path = ""
|
||||
mono_so_file = ""
|
||||
|
||||
if not mono_root and (is_android or is_javascript or is_ios):
|
||||
raise RuntimeError(
|
||||
"Mono installation directory not found; specify one manually with the 'mono_prefix' SCons parameter"
|
||||
)
|
||||
|
||||
if not mono_root and is_macos:
|
||||
# Try with some known directories under OSX
|
||||
hint_dirs = ["/Library/Frameworks/Mono.framework/Versions/Current", "/usr/local/var/homebrew/linked/mono"]
|
||||
for hint_dir in hint_dirs:
|
||||
if os.path.isdir(hint_dir):
|
||||
mono_root = hint_dir
|
||||
break
|
||||
|
||||
# We can't use pkg-config to link mono statically,
|
||||
# but we can still use it to find the mono root directory
|
||||
if not mono_root and mono_static:
|
||||
mono_root = pkgconfig_try_find_mono_root(mono_lib_names, sharedlib_ext)
|
||||
if not mono_root:
|
||||
raise RuntimeError(
|
||||
"Building with mono_static=yes, but failed to find the mono prefix with pkg-config; "
|
||||
+ "specify one manually with the 'mono_prefix' SCons parameter"
|
||||
)
|
||||
|
||||
if is_ios and not is_ios_sim:
|
||||
env_mono.Append(CPPDEFINES=["IOS_DEVICE"])
|
||||
|
||||
if mono_root:
|
||||
print("Found Mono root directory: " + mono_root)
|
||||
|
||||
mono_lib_path = os.path.join(mono_root, "lib")
|
||||
|
||||
env.Append(LIBPATH=[mono_lib_path])
|
||||
env_mono.Prepend(CPPPATH=os.path.join(mono_root, "include", "mono-2.0"))
|
||||
|
||||
mono_lib = find_name_in_dir_files(mono_lib_path, mono_lib_names, prefixes=["lib"], extensions=[".a"])
|
||||
|
||||
if not mono_lib:
|
||||
raise RuntimeError("Could not find mono library in: " + mono_lib_path)
|
||||
|
||||
env_mono.Append(CPPDEFINES=["_REENTRANT"])
|
||||
|
||||
if mono_static:
|
||||
if not is_javascript:
|
||||
env.Append(LINKFLAGS=["-rdynamic"])
|
||||
|
||||
mono_lib_file = os.path.join(mono_lib_path, "lib" + mono_lib + ".a")
|
||||
|
||||
if is_apple:
|
||||
if is_macos:
|
||||
env.Append(LINKFLAGS=["-Wl,-force_load," + mono_lib_file])
|
||||
else:
|
||||
arch = env["arch"]
|
||||
|
||||
def copy_mono_lib(libname_wo_ext):
|
||||
if is_ios_sim:
|
||||
copy_file(
|
||||
mono_lib_path,
|
||||
"#bin",
|
||||
libname_wo_ext + ".a",
|
||||
"%s.iphone.%s.simulator.a" % (libname_wo_ext, arch),
|
||||
)
|
||||
else:
|
||||
copy_file(
|
||||
mono_lib_path,
|
||||
"#bin",
|
||||
libname_wo_ext + ".a",
|
||||
"%s.iphone.%s.a" % (libname_wo_ext, arch),
|
||||
)
|
||||
|
||||
# Copy Mono libraries to the output folder. These are meant to be bundled with
|
||||
# the export templates and added to the Xcode project when exporting a game.
|
||||
copy_mono_lib("lib" + mono_lib)
|
||||
copy_mono_lib("libmono-native")
|
||||
copy_mono_lib("libmono-profiler-log")
|
||||
|
||||
if not is_ios_sim:
|
||||
copy_mono_lib("libmono-ee-interp")
|
||||
copy_mono_lib("libmono-icall-table")
|
||||
copy_mono_lib("libmono-ilgen")
|
||||
else:
|
||||
assert is_desktop(env["platform"]) or is_android or is_javascript
|
||||
env.Append(LINKFLAGS=["-Wl,-whole-archive", mono_lib_file, "-Wl,-no-whole-archive"])
|
||||
|
||||
if is_javascript:
|
||||
env.Append(LIBS=["mono-icall-table", "mono-native", "mono-ilgen", "mono-ee-interp"])
|
||||
|
||||
wasm_src_dir = os.path.join(mono_root, "src")
|
||||
if not os.path.isdir(wasm_src_dir):
|
||||
raise RuntimeError("Could not find mono wasm src directory")
|
||||
|
||||
# Ideally this should be defined only for 'driver.c', but I can't fight scons for another 2 hours
|
||||
env_mono.Append(CPPDEFINES=["CORE_BINDINGS"])
|
||||
|
||||
env_mono.add_source_files(
|
||||
env.modules_sources,
|
||||
[
|
||||
os.path.join(wasm_src_dir, "driver.c"),
|
||||
os.path.join(wasm_src_dir, "zlib-helper.c"),
|
||||
os.path.join(wasm_src_dir, "corebindings.c"),
|
||||
],
|
||||
)
|
||||
|
||||
env.Append(
|
||||
LINKFLAGS=[
|
||||
"--js-library",
|
||||
os.path.join(wasm_src_dir, "library_mono.js"),
|
||||
"--js-library",
|
||||
os.path.join(wasm_src_dir, "binding_support.js"),
|
||||
"--js-library",
|
||||
os.path.join(wasm_src_dir, "dotnet_support.js"),
|
||||
]
|
||||
)
|
||||
else:
|
||||
env.Append(LIBS=[mono_lib])
|
||||
|
||||
if is_macos:
|
||||
env.Append(LIBS=["iconv", "pthread"])
|
||||
elif is_android:
|
||||
pass # Nothing
|
||||
elif is_ios:
|
||||
pass # Nothing, linking is delegated to the exported Xcode project
|
||||
elif is_javascript:
|
||||
env.Append(LIBS=["m", "rt", "dl", "pthread"])
|
||||
else:
|
||||
env.Append(LIBS=["m", "rt", "dl", "pthread"])
|
||||
|
||||
if not mono_static:
|
||||
mono_so_file = find_file_in_dir(
|
||||
mono_lib_path, mono_lib_names, prefixes=["lib"], extensions=[sharedlib_ext]
|
||||
)
|
||||
|
||||
if not mono_so_file:
|
||||
raise RuntimeError("Could not find mono shared library in: " + mono_lib_path)
|
||||
else:
|
||||
assert not mono_static
|
||||
|
||||
# TODO: Add option to force using pkg-config
|
||||
print("Mono root directory not found. Using pkg-config instead")
|
||||
|
||||
env.ParseConfig("pkg-config monosgen-2 --libs")
|
||||
env_mono.ParseConfig("pkg-config monosgen-2 --cflags")
|
||||
|
||||
tmpenv = Environment()
|
||||
tmpenv.AppendENVPath("PKG_CONFIG_PATH", os.getenv("PKG_CONFIG_PATH"))
|
||||
tmpenv.ParseConfig("pkg-config monosgen-2 --libs-only-L")
|
||||
|
||||
for hint_dir in tmpenv["LIBPATH"]:
|
||||
file_found = find_file_in_dir(hint_dir, mono_lib_names, prefixes=["lib"], extensions=[sharedlib_ext])
|
||||
if file_found:
|
||||
mono_lib_path = hint_dir
|
||||
mono_so_file = file_found
|
||||
break
|
||||
|
||||
if not mono_so_file:
|
||||
raise RuntimeError("Could not find mono shared library in: " + str(tmpenv["LIBPATH"]))
|
||||
|
||||
if not mono_static:
|
||||
libs_output_dir = get_android_out_dir(env) if is_android else "#bin"
|
||||
copy_file(mono_lib_path, libs_output_dir, mono_so_file)
|
||||
|
||||
if not tools_enabled:
|
||||
if is_desktop(env["platform"]):
|
||||
if not mono_root:
|
||||
mono_root = (
|
||||
subprocess.check_output(["pkg-config", "mono-2", "--variable=prefix"]).decode("utf8").strip()
|
||||
)
|
||||
|
||||
make_template_dir(env, mono_root)
|
||||
elif is_android:
|
||||
# Compress Android Mono Config
|
||||
from . import make_android_mono_config
|
||||
|
||||
module_dir = os.getcwd()
|
||||
config_file_path = os.path.join(module_dir, "build_scripts", "mono_android_config.xml")
|
||||
make_android_mono_config.generate_compressed_config(config_file_path, "mono_gd/")
|
||||
|
||||
# Copy the required shared libraries
|
||||
copy_mono_shared_libs(env, mono_root, None)
|
||||
elif is_javascript:
|
||||
pass # No data directory for this platform
|
||||
elif is_ios:
|
||||
pass # No data directory for this platform
|
||||
|
||||
if copy_mono_root:
|
||||
if not mono_root:
|
||||
mono_root = subprocess.check_output(["pkg-config", "mono-2", "--variable=prefix"]).decode("utf8").strip()
|
||||
|
||||
if tools_enabled:
|
||||
# Only supported for editor builds.
|
||||
copy_mono_root_files(env, mono_root, mono_bcl)
|
||||
|
||||
|
||||
def make_template_dir(env, mono_root):
|
||||
from shutil import rmtree
|
||||
|
||||
platform = env["platform"]
|
||||
target = env["target"]
|
||||
|
||||
template_dir_name = ""
|
||||
|
||||
assert is_desktop(platform)
|
||||
|
||||
template_dir_name = "data.mono.%s.%s.%s" % (platform, env["bits"], target)
|
||||
|
||||
output_dir = Dir("#bin").abspath
|
||||
template_dir = os.path.join(output_dir, template_dir_name)
|
||||
|
||||
template_mono_root_dir = os.path.join(template_dir, "Mono")
|
||||
|
||||
if os.path.isdir(template_mono_root_dir):
|
||||
rmtree(template_mono_root_dir) # Clean first
|
||||
|
||||
# Copy etc/mono/
|
||||
|
||||
template_mono_config_dir = os.path.join(template_mono_root_dir, "etc", "mono")
|
||||
copy_mono_etc_dir(mono_root, template_mono_config_dir, platform)
|
||||
|
||||
# Copy the required shared libraries
|
||||
|
||||
copy_mono_shared_libs(env, mono_root, template_mono_root_dir)
|
||||
|
||||
|
||||
def copy_mono_root_files(env, mono_root, mono_bcl):
|
||||
from glob import glob
|
||||
from shutil import copy
|
||||
from shutil import rmtree
|
||||
|
||||
if not mono_root:
|
||||
raise RuntimeError("Mono installation directory not found")
|
||||
|
||||
output_dir = Dir("#bin").abspath
|
||||
editor_mono_root_dir = os.path.join(output_dir, "GodotSharp", "Mono")
|
||||
|
||||
if os.path.isdir(editor_mono_root_dir):
|
||||
rmtree(editor_mono_root_dir) # Clean first
|
||||
|
||||
# Copy etc/mono/
|
||||
|
||||
editor_mono_config_dir = os.path.join(editor_mono_root_dir, "etc", "mono")
|
||||
copy_mono_etc_dir(mono_root, editor_mono_config_dir, env["platform"])
|
||||
|
||||
# Copy the required shared libraries
|
||||
|
||||
copy_mono_shared_libs(env, mono_root, editor_mono_root_dir)
|
||||
|
||||
# Copy framework assemblies
|
||||
|
||||
mono_framework_dir = mono_bcl or os.path.join(mono_root, "lib", "mono", "4.5")
|
||||
mono_framework_facades_dir = os.path.join(mono_framework_dir, "Facades")
|
||||
|
||||
editor_mono_framework_dir = os.path.join(editor_mono_root_dir, "lib", "mono", "4.5")
|
||||
editor_mono_framework_facades_dir = os.path.join(editor_mono_framework_dir, "Facades")
|
||||
|
||||
if not os.path.isdir(editor_mono_framework_dir):
|
||||
os.makedirs(editor_mono_framework_dir)
|
||||
if not os.path.isdir(editor_mono_framework_facades_dir):
|
||||
os.makedirs(editor_mono_framework_facades_dir)
|
||||
|
||||
for assembly in glob(os.path.join(mono_framework_dir, "*.dll")):
|
||||
copy(assembly, editor_mono_framework_dir)
|
||||
for assembly in glob(os.path.join(mono_framework_facades_dir, "*.dll")):
|
||||
copy(assembly, editor_mono_framework_facades_dir)
|
||||
|
||||
|
||||
def copy_mono_etc_dir(mono_root, target_mono_config_dir, platform):
|
||||
from distutils.dir_util import copy_tree
|
||||
from glob import glob
|
||||
from shutil import copy
|
||||
|
||||
if not os.path.isdir(target_mono_config_dir):
|
||||
os.makedirs(target_mono_config_dir)
|
||||
|
||||
mono_etc_dir = os.path.join(mono_root, "etc", "mono")
|
||||
if not os.path.isdir(mono_etc_dir):
|
||||
mono_etc_dir = ""
|
||||
etc_hint_dirs = []
|
||||
if platform != "windows":
|
||||
etc_hint_dirs += ["/etc/mono", "/usr/local/etc/mono"]
|
||||
if "MONO_CFG_DIR" in os.environ:
|
||||
etc_hint_dirs += [os.path.join(os.environ["MONO_CFG_DIR"], "mono")]
|
||||
for etc_hint_dir in etc_hint_dirs:
|
||||
if os.path.isdir(etc_hint_dir):
|
||||
mono_etc_dir = etc_hint_dir
|
||||
break
|
||||
if not mono_etc_dir:
|
||||
raise RuntimeError("Mono installation etc directory not found")
|
||||
|
||||
copy_tree(os.path.join(mono_etc_dir, "2.0"), os.path.join(target_mono_config_dir, "2.0"))
|
||||
copy_tree(os.path.join(mono_etc_dir, "4.0"), os.path.join(target_mono_config_dir, "4.0"))
|
||||
copy_tree(os.path.join(mono_etc_dir, "4.5"), os.path.join(target_mono_config_dir, "4.5"))
|
||||
if os.path.isdir(os.path.join(mono_etc_dir, "mconfig")):
|
||||
copy_tree(os.path.join(mono_etc_dir, "mconfig"), os.path.join(target_mono_config_dir, "mconfig"))
|
||||
|
||||
for file in glob(os.path.join(mono_etc_dir, "*")):
|
||||
if os.path.isfile(file):
|
||||
copy(file, target_mono_config_dir)
|
||||
|
||||
|
||||
def copy_mono_shared_libs(env, mono_root, target_mono_root_dir):
|
||||
from shutil import copy
|
||||
|
||||
def copy_if_exists(src, dst):
|
||||
if os.path.isfile(src):
|
||||
copy(src, dst)
|
||||
|
||||
platform = env["platform"]
|
||||
|
||||
if platform == "windows":
|
||||
src_mono_bin_dir = os.path.join(mono_root, "bin")
|
||||
target_mono_bin_dir = os.path.join(target_mono_root_dir, "bin")
|
||||
|
||||
if not os.path.isdir(target_mono_bin_dir):
|
||||
os.makedirs(target_mono_bin_dir)
|
||||
|
||||
mono_posix_helper_file = find_file_in_dir(
|
||||
src_mono_bin_dir, ["MonoPosixHelper"], prefixes=["", "lib"], extensions=[".dll"]
|
||||
)
|
||||
copy(
|
||||
os.path.join(src_mono_bin_dir, mono_posix_helper_file),
|
||||
os.path.join(target_mono_bin_dir, "MonoPosixHelper.dll"),
|
||||
)
|
||||
|
||||
# For newer versions
|
||||
btls_dll_path = os.path.join(src_mono_bin_dir, "libmono-btls-shared.dll")
|
||||
if os.path.isfile(btls_dll_path):
|
||||
copy(btls_dll_path, target_mono_bin_dir)
|
||||
else:
|
||||
target_mono_lib_dir = (
|
||||
get_android_out_dir(env) if platform == "android" else os.path.join(target_mono_root_dir, "lib")
|
||||
)
|
||||
|
||||
if not os.path.isdir(target_mono_lib_dir):
|
||||
os.makedirs(target_mono_lib_dir)
|
||||
|
||||
src_mono_lib_dir = os.path.join(mono_root, "lib")
|
||||
|
||||
lib_file_names = []
|
||||
if platform == "osx":
|
||||
lib_file_names = [lib_name + ".dylib" for lib_name in ["libmono-btls-shared", "libMonoPosixHelper"]]
|
||||
|
||||
if os.path.isfile(os.path.join(src_mono_lib_dir, "libmono-native-compat.dylib")):
|
||||
lib_file_names += ["libmono-native-compat.dylib"]
|
||||
else:
|
||||
lib_file_names += ["libmono-native.dylib"]
|
||||
elif is_unix_like(platform):
|
||||
lib_file_names = [
|
||||
lib_name + ".so"
|
||||
for lib_name in [
|
||||
"libmono-btls-shared",
|
||||
"libmono-ee-interp",
|
||||
"libmono-native",
|
||||
"libMonoPosixHelper",
|
||||
"libmono-profiler-aot",
|
||||
"libmono-profiler-coverage",
|
||||
"libmono-profiler-log",
|
||||
"libMonoSupportW",
|
||||
]
|
||||
]
|
||||
|
||||
for lib_file_name in lib_file_names:
|
||||
copy_if_exists(os.path.join(src_mono_lib_dir, lib_file_name), target_mono_lib_dir)
|
||||
|
||||
|
||||
def pkgconfig_try_find_mono_root(mono_lib_names, sharedlib_ext):
|
||||
tmpenv = Environment()
|
||||
tmpenv.AppendENVPath("PKG_CONFIG_PATH", os.getenv("PKG_CONFIG_PATH"))
|
||||
tmpenv.ParseConfig("pkg-config monosgen-2 --libs-only-L")
|
||||
for hint_dir in tmpenv["LIBPATH"]:
|
||||
name_found = find_name_in_dir_files(hint_dir, mono_lib_names, prefixes=["lib"], extensions=[sharedlib_ext])
|
||||
if name_found and os.path.isdir(os.path.join(hint_dir, "..", "include", "mono-2.0")):
|
||||
return os.path.join(hint_dir, "..")
|
||||
return ""
|
119
build_scripts/mono_reg_utils.py
Normal file
119
build_scripts/mono_reg_utils.py
Normal file
@ -0,0 +1,119 @@
|
||||
import os
|
||||
import platform
|
||||
|
||||
from compat import decode_utf8
|
||||
|
||||
if os.name == "nt":
|
||||
import sys
|
||||
|
||||
if sys.version_info < (3,):
|
||||
import _winreg as winreg
|
||||
else:
|
||||
import winreg
|
||||
|
||||
|
||||
def _reg_open_key(key, subkey):
|
||||
try:
|
||||
return winreg.OpenKey(key, subkey)
|
||||
except (WindowsError, OSError):
|
||||
if platform.architecture()[0] == "32bit":
|
||||
bitness_sam = winreg.KEY_WOW64_64KEY
|
||||
else:
|
||||
bitness_sam = winreg.KEY_WOW64_32KEY
|
||||
return winreg.OpenKey(key, subkey, 0, winreg.KEY_READ | bitness_sam)
|
||||
|
||||
|
||||
def _reg_open_key_bits(key, subkey, bits):
|
||||
sam = winreg.KEY_READ
|
||||
|
||||
if platform.architecture()[0] == "32bit":
|
||||
if bits == "64":
|
||||
# Force 32bit process to search in 64bit registry
|
||||
sam |= winreg.KEY_WOW64_64KEY
|
||||
else:
|
||||
if bits == "32":
|
||||
# Force 64bit process to search in 32bit registry
|
||||
sam |= winreg.KEY_WOW64_32KEY
|
||||
|
||||
return winreg.OpenKey(key, subkey, 0, sam)
|
||||
|
||||
|
||||
def _find_mono_in_reg(subkey, bits):
|
||||
try:
|
||||
with _reg_open_key_bits(winreg.HKEY_LOCAL_MACHINE, subkey, bits) as hKey:
|
||||
value = winreg.QueryValueEx(hKey, "SdkInstallRoot")[0]
|
||||
return value
|
||||
except (WindowsError, OSError):
|
||||
return None
|
||||
|
||||
|
||||
def _find_mono_in_reg_old(subkey, bits):
|
||||
try:
|
||||
with _reg_open_key_bits(winreg.HKEY_LOCAL_MACHINE, subkey, bits) as hKey:
|
||||
default_clr = winreg.QueryValueEx(hKey, "DefaultCLR")[0]
|
||||
if default_clr:
|
||||
return _find_mono_in_reg(subkey + "\\" + default_clr, bits)
|
||||
return None
|
||||
except (WindowsError, EnvironmentError):
|
||||
return None
|
||||
|
||||
|
||||
def find_mono_root_dir(bits):
|
||||
root_dir = _find_mono_in_reg(r"SOFTWARE\Mono", bits)
|
||||
if root_dir is not None:
|
||||
return str(root_dir)
|
||||
root_dir = _find_mono_in_reg_old(r"SOFTWARE\Novell\Mono", bits)
|
||||
if root_dir is not None:
|
||||
return str(root_dir)
|
||||
return ""
|
||||
|
||||
|
||||
def find_msbuild_tools_path_reg():
|
||||
import subprocess
|
||||
|
||||
vswhere = os.getenv("PROGRAMFILES(X86)")
|
||||
if not vswhere:
|
||||
vswhere = os.getenv("PROGRAMFILES")
|
||||
vswhere += r"\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||
|
||||
vswhere_args = ["-latest", "-products", "*", "-requires", "Microsoft.Component.MSBuild"]
|
||||
|
||||
try:
|
||||
lines = subprocess.check_output([vswhere] + vswhere_args).splitlines()
|
||||
|
||||
for line in lines:
|
||||
parts = decode_utf8(line).split(":", 1)
|
||||
|
||||
if len(parts) < 2 or parts[0] != "installationPath":
|
||||
continue
|
||||
|
||||
val = parts[1].strip()
|
||||
|
||||
if not val:
|
||||
raise ValueError("Value of `installationPath` entry is empty")
|
||||
|
||||
# Since VS2019, the directory is simply named "Current"
|
||||
msbuild_dir = os.path.join(val, "MSBuild\\Current\\Bin")
|
||||
if os.path.isdir(msbuild_dir):
|
||||
return msbuild_dir
|
||||
|
||||
# Directory name "15.0" is used in VS 2017
|
||||
return os.path.join(val, "MSBuild\\15.0\\Bin")
|
||||
|
||||
raise ValueError("Cannot find `installationPath` entry")
|
||||
except ValueError as e:
|
||||
print("Error reading output from vswhere: " + e.message)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(e.output)
|
||||
except OSError as e:
|
||||
print(e)
|
||||
|
||||
# Try to find 14.0 in the Registry
|
||||
|
||||
try:
|
||||
subkey = r"SOFTWARE\Microsoft\MSBuild\ToolsVersions\14.0"
|
||||
with _reg_open_key(winreg.HKEY_LOCAL_MACHINE, subkey) as hKey:
|
||||
value = winreg.QueryValueEx(hKey, "MSBuildToolsPath")[0]
|
||||
return value
|
||||
except (WindowsError, OSError):
|
||||
return ""
|
148
build_scripts/solution_builder.py
Normal file
148
build_scripts/solution_builder.py
Normal file
@ -0,0 +1,148 @@
|
||||
import os
|
||||
|
||||
|
||||
verbose = False
|
||||
|
||||
|
||||
def find_dotnet_cli():
|
||||
import os.path
|
||||
|
||||
if os.name == "nt":
|
||||
windows_exts = os.environ["PATHEXT"]
|
||||
windows_exts = windows_exts.split(os.pathsep) if windows_exts else []
|
||||
|
||||
for hint_dir in os.environ["PATH"].split(os.pathsep):
|
||||
hint_dir = hint_dir.strip('"')
|
||||
hint_path = os.path.join(hint_dir, "dotnet")
|
||||
if os.path.isfile(hint_path) and os.access(hint_path, os.X_OK):
|
||||
return hint_path
|
||||
if os.path.isfile(hint_path + ".exe") and os.access(hint_path + ".exe", os.X_OK):
|
||||
return hint_path + ".exe"
|
||||
else:
|
||||
for hint_dir in os.environ["PATH"].split(os.pathsep):
|
||||
hint_dir = hint_dir.strip('"')
|
||||
hint_path = os.path.join(hint_dir, "dotnet")
|
||||
if os.path.isfile(hint_path) and os.access(hint_path, os.X_OK):
|
||||
return hint_path
|
||||
|
||||
|
||||
def find_msbuild_unix():
|
||||
import os.path
|
||||
import sys
|
||||
|
||||
hint_dirs = []
|
||||
if sys.platform == "darwin":
|
||||
hint_dirs[:0] = [
|
||||
"/Library/Frameworks/Mono.framework/Versions/Current/bin",
|
||||
"/usr/local/var/homebrew/linked/mono/bin",
|
||||
]
|
||||
|
||||
for hint_dir in hint_dirs:
|
||||
hint_path = os.path.join(hint_dir, "msbuild")
|
||||
if os.path.isfile(hint_path):
|
||||
return hint_path
|
||||
elif os.path.isfile(hint_path + ".exe"):
|
||||
return hint_path + ".exe"
|
||||
|
||||
for hint_dir in os.environ["PATH"].split(os.pathsep):
|
||||
hint_dir = hint_dir.strip('"')
|
||||
hint_path = os.path.join(hint_dir, "msbuild")
|
||||
if os.path.isfile(hint_path) and os.access(hint_path, os.X_OK):
|
||||
return hint_path
|
||||
if os.path.isfile(hint_path + ".exe") and os.access(hint_path + ".exe", os.X_OK):
|
||||
return hint_path + ".exe"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def find_msbuild_windows(env):
|
||||
from .mono_reg_utils import find_mono_root_dir, find_msbuild_tools_path_reg
|
||||
|
||||
mono_root = env["mono_prefix"] or find_mono_root_dir(env["bits"])
|
||||
|
||||
if not mono_root:
|
||||
raise RuntimeError("Cannot find mono root directory")
|
||||
|
||||
mono_bin_dir = os.path.join(mono_root, "bin")
|
||||
msbuild_mono = os.path.join(mono_bin_dir, "msbuild.bat")
|
||||
|
||||
msbuild_tools_path = find_msbuild_tools_path_reg()
|
||||
|
||||
if msbuild_tools_path:
|
||||
return (os.path.join(msbuild_tools_path, "MSBuild.exe"), {})
|
||||
|
||||
if os.path.isfile(msbuild_mono):
|
||||
# The (Csc/Vbc/Fsc)ToolExe environment variables are required when
|
||||
# building with Mono's MSBuild. They must point to the batch files
|
||||
# in Mono's bin directory to make sure they are executed with Mono.
|
||||
mono_msbuild_env = {
|
||||
"CscToolExe": os.path.join(mono_bin_dir, "csc.bat"),
|
||||
"VbcToolExe": os.path.join(mono_bin_dir, "vbc.bat"),
|
||||
"FscToolExe": os.path.join(mono_bin_dir, "fsharpc.bat"),
|
||||
}
|
||||
return (msbuild_mono, mono_msbuild_env)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def run_command(command, args, env_override=None, name=None):
|
||||
def cmd_args_to_str(cmd_args):
|
||||
return " ".join([arg if not " " in arg else '"%s"' % arg for arg in cmd_args])
|
||||
|
||||
args = [command] + args
|
||||
|
||||
if name is None:
|
||||
name = os.path.basename(command)
|
||||
|
||||
if verbose:
|
||||
print("Running '%s': %s" % (name, cmd_args_to_str(args)))
|
||||
|
||||
import subprocess
|
||||
|
||||
try:
|
||||
if env_override is None:
|
||||
subprocess.check_call(args)
|
||||
else:
|
||||
subprocess.check_call(args, env=env_override)
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise RuntimeError("'%s' exited with error code: %s" % (name, e.returncode))
|
||||
|
||||
|
||||
def build_solution(env, solution_path, build_config, extra_msbuild_args=[]):
|
||||
global verbose
|
||||
verbose = env["verbose"]
|
||||
|
||||
msbuild_env = os.environ.copy()
|
||||
|
||||
# Needed when running from Developer Command Prompt for VS
|
||||
if "PLATFORM" in msbuild_env:
|
||||
del msbuild_env["PLATFORM"]
|
||||
|
||||
msbuild_args = []
|
||||
|
||||
dotnet_cli = find_dotnet_cli()
|
||||
|
||||
if dotnet_cli:
|
||||
msbuild_path = dotnet_cli
|
||||
msbuild_args += ["msbuild"] # `dotnet msbuild` command
|
||||
else:
|
||||
# Find MSBuild
|
||||
if os.name == "nt":
|
||||
msbuild_info = find_msbuild_windows(env)
|
||||
if msbuild_info is None:
|
||||
raise RuntimeError("Cannot find MSBuild executable")
|
||||
msbuild_path = msbuild_info[0]
|
||||
msbuild_env.update(msbuild_info[1])
|
||||
else:
|
||||
msbuild_path = find_msbuild_unix()
|
||||
if msbuild_path is None:
|
||||
raise RuntimeError("Cannot find MSBuild executable")
|
||||
|
||||
print("MSBuild path: " + msbuild_path)
|
||||
|
||||
# Build solution
|
||||
|
||||
msbuild_args += [solution_path, "/restore", "/t:Build", "/p:Configuration=" + build_config]
|
||||
msbuild_args += extra_msbuild_args
|
||||
|
||||
run_command(msbuild_path, msbuild_args, env_override=msbuild_env, name="msbuild")
|
37
build_scripts/tls_configure.py
Normal file
37
build_scripts/tls_configure.py
Normal file
@ -0,0 +1,37 @@
|
||||
from __future__ import print_function
|
||||
|
||||
|
||||
def supported(result):
|
||||
return "supported" if result else "not supported"
|
||||
|
||||
|
||||
def check_cxx11_thread_local(conf):
|
||||
print("Checking for `thread_local` support...", end=" ")
|
||||
result = conf.TryCompile("thread_local int foo = 0; int main() { return foo; }", ".cpp")
|
||||
print(supported(result))
|
||||
return bool(result)
|
||||
|
||||
|
||||
def check_declspec_thread(conf):
|
||||
print("Checking for `__declspec(thread)` support...", end=" ")
|
||||
result = conf.TryCompile("__declspec(thread) int foo = 0; int main() { return foo; }", ".cpp")
|
||||
print(supported(result))
|
||||
return bool(result)
|
||||
|
||||
|
||||
def check_gcc___thread(conf):
|
||||
print("Checking for `__thread` support...", end=" ")
|
||||
result = conf.TryCompile("__thread int foo = 0; int main() { return foo; }", ".cpp")
|
||||
print(supported(result))
|
||||
return bool(result)
|
||||
|
||||
|
||||
def configure(conf):
|
||||
if check_cxx11_thread_local(conf):
|
||||
conf.env.Append(CPPDEFINES=["HAVE_CXX11_THREAD_LOCAL"])
|
||||
else:
|
||||
if conf.env.msvc:
|
||||
if check_declspec_thread(conf):
|
||||
conf.env.Append(CPPDEFINES=["HAVE_DECLSPEC_THREAD"])
|
||||
elif check_gcc___thread(conf):
|
||||
conf.env.Append(CPPDEFINES=["HAVE_GCC___THREAD"])
|
247
class_db_api_json.cpp
Normal file
247
class_db_api_json.cpp
Normal file
@ -0,0 +1,247 @@
|
||||
/**************************************************************************/
|
||||
/* class_db_api_json.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. */
|
||||
/**************************************************************************/
|
||||
|
||||
#include "class_db_api_json.h"
|
||||
|
||||
#ifdef DEBUG_METHODS_ENABLED
|
||||
|
||||
#include "core/io/json.h"
|
||||
#include "core/os/file_access.h"
|
||||
#include "core/project_settings.h"
|
||||
#include "core/version.h"
|
||||
|
||||
void class_db_api_to_json(const String &p_output_file, ClassDB::APIType p_api) {
|
||||
Dictionary classes_dict;
|
||||
|
||||
List<StringName> names;
|
||||
|
||||
const StringName *k = NULL;
|
||||
|
||||
while ((k = ClassDB::classes.next(k))) {
|
||||
names.push_back(*k);
|
||||
}
|
||||
//must be alphabetically sorted for hash to compute
|
||||
names.sort_custom<StringName::AlphCompare>();
|
||||
|
||||
for (List<StringName>::Element *E = names.front(); E; E = E->next()) {
|
||||
ClassDB::ClassInfo *t = ClassDB::classes.getptr(E->get());
|
||||
ERR_FAIL_COND(!t);
|
||||
if (t->api != p_api || !t->exposed)
|
||||
continue;
|
||||
|
||||
Dictionary class_dict;
|
||||
classes_dict[t->name] = class_dict;
|
||||
|
||||
class_dict["inherits"] = t->inherits;
|
||||
|
||||
{ //methods
|
||||
|
||||
List<StringName> snames;
|
||||
|
||||
k = NULL;
|
||||
|
||||
while ((k = t->method_map.next(k))) {
|
||||
String name = k->operator String();
|
||||
|
||||
ERR_CONTINUE(name.empty());
|
||||
|
||||
if (name[0] == '_')
|
||||
continue; // Ignore non-virtual methods that start with an underscore
|
||||
|
||||
snames.push_back(*k);
|
||||
}
|
||||
|
||||
snames.sort_custom<StringName::AlphCompare>();
|
||||
|
||||
Array methods;
|
||||
|
||||
for (List<StringName>::Element *F = snames.front(); F; F = F->next()) {
|
||||
Dictionary method_dict;
|
||||
methods.push_back(method_dict);
|
||||
|
||||
MethodBind *mb = t->method_map[F->get()];
|
||||
method_dict["name"] = mb->get_name();
|
||||
method_dict["argument_count"] = mb->get_argument_count();
|
||||
method_dict["return_type"] = mb->get_argument_type(-1);
|
||||
|
||||
Array arguments;
|
||||
method_dict["arguments"] = arguments;
|
||||
|
||||
for (int i = 0; i < mb->get_argument_count(); i++) {
|
||||
Dictionary argument_dict;
|
||||
arguments.push_back(argument_dict);
|
||||
const PropertyInfo info = mb->get_argument_info(i);
|
||||
argument_dict["type"] = info.type;
|
||||
argument_dict["name"] = info.name;
|
||||
argument_dict["hint"] = info.hint;
|
||||
argument_dict["hint_string"] = info.hint_string;
|
||||
}
|
||||
|
||||
method_dict["default_argument_count"] = mb->get_default_argument_count();
|
||||
|
||||
Array default_arguments;
|
||||
method_dict["default_arguments"] = default_arguments;
|
||||
|
||||
for (int i = 0; i < mb->get_default_argument_count(); i++) {
|
||||
Dictionary default_argument_dict;
|
||||
default_arguments.push_back(default_argument_dict);
|
||||
//hash should not change, i hope for tis
|
||||
Variant da = mb->get_default_argument(i);
|
||||
default_argument_dict["value"] = da;
|
||||
}
|
||||
|
||||
method_dict["hint_flags"] = mb->get_hint_flags();
|
||||
}
|
||||
|
||||
if (!methods.empty()) {
|
||||
class_dict["methods"] = methods;
|
||||
}
|
||||
}
|
||||
|
||||
{ //constants
|
||||
|
||||
List<StringName> snames;
|
||||
|
||||
k = NULL;
|
||||
|
||||
while ((k = t->constant_map.next(k))) {
|
||||
snames.push_back(*k);
|
||||
}
|
||||
|
||||
snames.sort_custom<StringName::AlphCompare>();
|
||||
|
||||
Array constants;
|
||||
|
||||
for (List<StringName>::Element *F = snames.front(); F; F = F->next()) {
|
||||
Dictionary constant_dict;
|
||||
constants.push_back(constant_dict);
|
||||
|
||||
constant_dict["name"] = F->get();
|
||||
constant_dict["value"] = t->constant_map[F->get()];
|
||||
}
|
||||
|
||||
if (!constants.empty()) {
|
||||
class_dict["constants"] = constants;
|
||||
}
|
||||
}
|
||||
|
||||
{ //signals
|
||||
|
||||
List<StringName> snames;
|
||||
|
||||
k = NULL;
|
||||
|
||||
while ((k = t->signal_map.next(k))) {
|
||||
snames.push_back(*k);
|
||||
}
|
||||
|
||||
snames.sort_custom<StringName::AlphCompare>();
|
||||
|
||||
Array signals;
|
||||
|
||||
for (List<StringName>::Element *F = snames.front(); F; F = F->next()) {
|
||||
Dictionary signal_dict;
|
||||
signals.push_back(signal_dict);
|
||||
|
||||
MethodInfo &mi = t->signal_map[F->get()];
|
||||
signal_dict["name"] = F->get();
|
||||
|
||||
Array arguments;
|
||||
signal_dict["arguments"] = arguments;
|
||||
for (int i = 0; i < mi.arguments.size(); i++) {
|
||||
Dictionary argument_dict;
|
||||
arguments.push_back(argument_dict);
|
||||
argument_dict["type"] = mi.arguments[i].type;
|
||||
}
|
||||
}
|
||||
|
||||
if (!signals.empty()) {
|
||||
class_dict["signals"] = signals;
|
||||
}
|
||||
}
|
||||
|
||||
{ //properties
|
||||
|
||||
List<StringName> snames;
|
||||
|
||||
k = NULL;
|
||||
|
||||
while ((k = t->property_setget.next(k))) {
|
||||
snames.push_back(*k);
|
||||
}
|
||||
|
||||
snames.sort_custom<StringName::AlphCompare>();
|
||||
|
||||
Array properties;
|
||||
|
||||
for (List<StringName>::Element *F = snames.front(); F; F = F->next()) {
|
||||
Dictionary property_dict;
|
||||
properties.push_back(property_dict);
|
||||
|
||||
ClassDB::PropertySetGet *psg = t->property_setget.getptr(F->get());
|
||||
|
||||
property_dict["name"] = F->get();
|
||||
property_dict["setter"] = psg->setter;
|
||||
property_dict["getter"] = psg->getter;
|
||||
}
|
||||
|
||||
if (!properties.empty()) {
|
||||
class_dict["property_setget"] = properties;
|
||||
}
|
||||
}
|
||||
|
||||
Array property_list;
|
||||
|
||||
//property list
|
||||
for (List<PropertyInfo>::Element *F = t->property_list.front(); F; F = F->next()) {
|
||||
Dictionary property_dict;
|
||||
property_list.push_back(property_dict);
|
||||
|
||||
property_dict["name"] = F->get().name;
|
||||
property_dict["type"] = F->get().type;
|
||||
property_dict["hint"] = F->get().hint;
|
||||
property_dict["hint_string"] = F->get().hint_string;
|
||||
property_dict["usage"] = F->get().usage;
|
||||
}
|
||||
|
||||
if (!property_list.empty()) {
|
||||
class_dict["property_list"] = property_list;
|
||||
}
|
||||
}
|
||||
|
||||
FileAccessRef f = FileAccess::open(p_output_file, FileAccess::WRITE);
|
||||
ERR_FAIL_COND_MSG(!f, "Cannot open file '" + p_output_file + "'.");
|
||||
f->store_string(JSON::print(classes_dict, /*indent: */ "\t"));
|
||||
f->close();
|
||||
|
||||
print_line(String() + "ClassDB API JSON written to: " + ProjectSettings::get_singleton()->globalize_path(p_output_file));
|
||||
}
|
||||
|
||||
#endif // DEBUG_METHODS_ENABLED
|
46
class_db_api_json.h
Normal file
46
class_db_api_json.h
Normal file
@ -0,0 +1,46 @@
|
||||
/**************************************************************************/
|
||||
/* class_db_api_json.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. */
|
||||
/**************************************************************************/
|
||||
|
||||
#ifndef CLASS_DB_API_JSON_H
|
||||
#define CLASS_DB_API_JSON_H
|
||||
|
||||
// 'core/method_bind.h' defines DEBUG_METHODS_ENABLED, but it looks like we
|
||||
// cannot include it here. That's why we include it through 'core/class_db.h'.
|
||||
#include "core/class_db.h"
|
||||
|
||||
#ifdef DEBUG_METHODS_ENABLED
|
||||
|
||||
#include "core/ustring.h"
|
||||
|
||||
void class_db_api_to_json(const String &p_output_file, ClassDB::APIType p_api);
|
||||
|
||||
#endif // DEBUG_METHODS_ENABLED
|
||||
|
||||
#endif // CLASS_DB_API_JSON_H
|
77
config.py
Normal file
77
config.py
Normal file
@ -0,0 +1,77 @@
|
||||
supported_platforms = ["windows", "osx", "x11", "server", "android", "haiku", "javascript", "iphone"]
|
||||
|
||||
|
||||
def can_build(env, platform):
|
||||
return not env["arch"].startswith("rv")
|
||||
|
||||
|
||||
def configure(env):
|
||||
platform = env["platform"]
|
||||
|
||||
if platform not in supported_platforms:
|
||||
raise RuntimeError("This module does not currently support building for this platform")
|
||||
|
||||
env.use_ptrcall = True
|
||||
env.add_module_version_string("mono")
|
||||
|
||||
from SCons.Script import BoolVariable, PathVariable, Variables, Help
|
||||
|
||||
default_mono_static = platform in ["iphone", "javascript"]
|
||||
default_mono_bundles_zlib = platform in ["javascript"]
|
||||
|
||||
envvars = Variables()
|
||||
envvars.Add(
|
||||
PathVariable(
|
||||
"mono_prefix",
|
||||
"Path to the Mono installation directory for the target platform and architecture",
|
||||
"",
|
||||
PathVariable.PathAccept,
|
||||
)
|
||||
)
|
||||
envvars.Add(
|
||||
PathVariable(
|
||||
"mono_bcl",
|
||||
"Path to a custom Mono BCL (Base Class Library) directory for the target platform",
|
||||
"",
|
||||
PathVariable.PathAccept,
|
||||
)
|
||||
)
|
||||
envvars.Add(BoolVariable("mono_static", "Statically link Mono", default_mono_static))
|
||||
envvars.Add(BoolVariable("mono_glue", "Build with the Mono glue sources", True))
|
||||
envvars.Add(BoolVariable("build_cil", "Build C# solutions", True))
|
||||
envvars.Add(
|
||||
BoolVariable("copy_mono_root", "Make a copy of the Mono installation directory to bundle with the editor", True)
|
||||
)
|
||||
|
||||
# TODO: It would be great if this could be detected automatically instead
|
||||
envvars.Add(
|
||||
BoolVariable(
|
||||
"mono_bundles_zlib", "Specify if the Mono runtime was built with bundled zlib", default_mono_bundles_zlib
|
||||
)
|
||||
)
|
||||
|
||||
envvars.Update(env)
|
||||
Help(envvars.GenerateHelpText(env))
|
||||
|
||||
if env["mono_bundles_zlib"]:
|
||||
# Mono may come with zlib bundled for WASM or on newer version when built with MinGW.
|
||||
print("This Mono runtime comes with zlib bundled. Disabling 'builtin_zlib'...")
|
||||
env["builtin_zlib"] = False
|
||||
thirdparty_zlib_dir = "#thirdparty/zlib/"
|
||||
env.Prepend(CPPPATH=[thirdparty_zlib_dir])
|
||||
|
||||
|
||||
def get_doc_classes():
|
||||
return [
|
||||
"CSharpScript",
|
||||
"GodotSharp",
|
||||
]
|
||||
|
||||
|
||||
def get_doc_path():
|
||||
return "doc_classes"
|
||||
|
||||
|
||||
def is_enabled():
|
||||
# The module is disabled by default. Use module_mono_enabled=yes to enable it.
|
||||
return False
|
3482
csharp_script.cpp
Normal file
3482
csharp_script.cpp
Normal file
File diff suppressed because it is too large
Load Diff
496
csharp_script.h
Normal file
496
csharp_script.h
Normal file
@ -0,0 +1,496 @@
|
||||
/**************************************************************************/
|
||||
/* csharp_script.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. */
|
||||
/**************************************************************************/
|
||||
|
||||
#ifndef CSHARP_SCRIPT_H
|
||||
#define CSHARP_SCRIPT_H
|
||||
|
||||
#include "core/io/resource_loader.h"
|
||||
#include "core/io/resource_saver.h"
|
||||
#include "core/script_language.h"
|
||||
#include "core/self_list.h"
|
||||
|
||||
#include "mono_gc_handle.h"
|
||||
#include "mono_gd/gd_mono.h"
|
||||
#include "mono_gd/gd_mono_header.h"
|
||||
#include "mono_gd/gd_mono_internals.h"
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
#include "editor/editor_plugin.h"
|
||||
#endif
|
||||
|
||||
class CSharpScript;
|
||||
class CSharpInstance;
|
||||
class CSharpLanguage;
|
||||
|
||||
#ifdef NO_SAFE_CAST
|
||||
template <typename TScriptInstance, typename TScriptLanguage>
|
||||
TScriptInstance *cast_script_instance(ScriptInstance *p_inst) {
|
||||
if (!p_inst)
|
||||
return NULL;
|
||||
return p_inst->get_language() == TScriptLanguage::get_singleton() ? static_cast<TScriptInstance *>(p_inst) : NULL;
|
||||
}
|
||||
#else
|
||||
template <typename TScriptInstance, typename TScriptLanguage>
|
||||
TScriptInstance *cast_script_instance(ScriptInstance *p_inst) {
|
||||
return dynamic_cast<TScriptInstance *>(p_inst);
|
||||
}
|
||||
#endif
|
||||
|
||||
#define CAST_CSHARP_INSTANCE(m_inst) (cast_script_instance<CSharpInstance, CSharpLanguage>(m_inst))
|
||||
|
||||
class CSharpScript : public Script {
|
||||
GDCLASS(CSharpScript, Script);
|
||||
|
||||
friend class CSharpInstance;
|
||||
friend class CSharpLanguage;
|
||||
friend struct CSharpScriptDepSort;
|
||||
|
||||
bool tool;
|
||||
bool valid;
|
||||
bool reload_invalidated;
|
||||
|
||||
bool builtin;
|
||||
|
||||
GDMonoClass *base;
|
||||
GDMonoClass *native;
|
||||
GDMonoClass *script_class;
|
||||
|
||||
Ref<CSharpScript> base_cache; // TODO what's this for?
|
||||
|
||||
Set<Object *> instances;
|
||||
|
||||
#ifdef GD_MONO_HOT_RELOAD
|
||||
struct StateBackup {
|
||||
// TODO
|
||||
// Replace with buffer containing the serialized state of managed scripts.
|
||||
// Keep variant state backup to use only with script instance placeholders.
|
||||
List<Pair<StringName, Variant>> properties;
|
||||
};
|
||||
|
||||
Set<ObjectID> pending_reload_instances;
|
||||
Map<ObjectID, StateBackup> pending_reload_state;
|
||||
StringName tied_class_name_for_reload;
|
||||
StringName tied_class_namespace_for_reload;
|
||||
#endif
|
||||
|
||||
String source;
|
||||
StringName name;
|
||||
|
||||
SelfList<CSharpScript> script_list;
|
||||
|
||||
struct Argument {
|
||||
String name;
|
||||
Variant::Type type;
|
||||
};
|
||||
|
||||
Map<StringName, Vector<Argument>> _signals;
|
||||
bool signals_invalidated;
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
List<PropertyInfo> exported_members_cache; // members_cache
|
||||
Map<StringName, Variant> exported_members_defval_cache; // member_default_values_cache
|
||||
Set<PlaceHolderScriptInstance *> placeholders;
|
||||
bool source_changed_cache;
|
||||
bool placeholder_fallback_enabled;
|
||||
bool exports_invalidated;
|
||||
void _update_exports_values(Map<StringName, Variant> &values, List<PropertyInfo> &propnames);
|
||||
void _update_member_info_no_exports();
|
||||
virtual void _placeholder_erased(PlaceHolderScriptInstance *p_placeholder);
|
||||
#endif
|
||||
|
||||
#if defined(TOOLS_ENABLED) || defined(DEBUG_ENABLED)
|
||||
Set<StringName> exported_members_names;
|
||||
#endif
|
||||
|
||||
OrderedHashMap<StringName, PropertyInfo> member_info;
|
||||
|
||||
void _clear();
|
||||
|
||||
void load_script_signals(GDMonoClass *p_class, GDMonoClass *p_native_class);
|
||||
bool _get_signal(GDMonoClass *p_class, GDMonoClass *p_delegate, Vector<Argument> ¶ms);
|
||||
|
||||
bool _update_exports(PlaceHolderScriptInstance *p_instance_to_update = nullptr);
|
||||
|
||||
bool _get_member_export(IMonoClassMember *p_member, bool p_inspect_export, PropertyInfo &r_prop_info, bool &r_exported);
|
||||
#ifdef TOOLS_ENABLED
|
||||
static int _try_get_member_export_hint(IMonoClassMember *p_member, ManagedType p_type, Variant::Type p_variant_type, bool p_allow_generics, PropertyHint &r_hint, String &r_hint_string);
|
||||
#endif
|
||||
|
||||
CSharpInstance *_create_instance(const Variant **p_args, int p_argcount, Object *p_owner, bool p_isref, Variant::CallError &r_error);
|
||||
Variant _new(const Variant **p_args, int p_argcount, Variant::CallError &r_error);
|
||||
|
||||
// Do not use unless you know what you are doing
|
||||
friend void GDMonoInternals::tie_managed_to_unmanaged(MonoObject *, Object *);
|
||||
static Ref<CSharpScript> create_for_managed_type(GDMonoClass *p_class, GDMonoClass *p_native);
|
||||
static void initialize_for_managed_type(Ref<CSharpScript> p_script, GDMonoClass *p_class, GDMonoClass *p_native);
|
||||
|
||||
protected:
|
||||
static void _bind_methods();
|
||||
|
||||
Variant call(const StringName &p_method, const Variant **p_args, int p_argcount, Variant::CallError &r_error);
|
||||
virtual void _resource_path_changed();
|
||||
bool _get(const StringName &p_name, Variant &r_ret) const;
|
||||
bool _set(const StringName &p_name, const Variant &p_value);
|
||||
void _get_property_list(List<PropertyInfo> *p_properties) const;
|
||||
|
||||
public:
|
||||
virtual bool can_instance() const;
|
||||
virtual StringName get_instance_base_type() const;
|
||||
virtual ScriptInstance *instance_create(Object *p_this);
|
||||
virtual PlaceHolderScriptInstance *placeholder_instance_create(Object *p_this);
|
||||
virtual bool instance_has(const Object *p_this) const;
|
||||
|
||||
virtual bool has_source_code() const;
|
||||
virtual String get_source_code() const;
|
||||
virtual void set_source_code(const String &p_code);
|
||||
|
||||
virtual Error reload(bool p_keep_state = false);
|
||||
|
||||
virtual bool has_script_signal(const StringName &p_signal) const;
|
||||
virtual void get_script_signal_list(List<MethodInfo> *r_signals) const;
|
||||
|
||||
virtual bool get_property_default_value(const StringName &p_property, Variant &r_value) const;
|
||||
virtual void get_script_property_list(List<PropertyInfo> *p_list) const;
|
||||
virtual void update_exports();
|
||||
|
||||
virtual void get_members(Set<StringName> *p_members);
|
||||
|
||||
virtual bool is_tool() const { return tool; }
|
||||
virtual bool is_valid() const { return valid; }
|
||||
|
||||
bool inherits_script(const Ref<Script> &p_script) const;
|
||||
|
||||
virtual Ref<Script> get_base_script() const;
|
||||
virtual ScriptLanguage *get_language() const;
|
||||
|
||||
virtual void get_script_method_list(List<MethodInfo> *p_list) const;
|
||||
bool has_method(const StringName &p_method) const;
|
||||
MethodInfo get_method_info(const StringName &p_method) const;
|
||||
|
||||
virtual int get_member_line(const StringName &p_member) const;
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
virtual bool is_placeholder_fallback_enabled() const { return placeholder_fallback_enabled; }
|
||||
#endif
|
||||
|
||||
Error load_source_code(const String &p_path);
|
||||
|
||||
StringName get_script_name() const;
|
||||
|
||||
CSharpScript();
|
||||
~CSharpScript();
|
||||
};
|
||||
|
||||
class CSharpInstance : public ScriptInstance {
|
||||
friend class CSharpScript;
|
||||
friend class CSharpLanguage;
|
||||
|
||||
Object *owner;
|
||||
bool base_ref;
|
||||
bool ref_dying;
|
||||
bool unsafe_referenced;
|
||||
bool predelete_notified;
|
||||
bool destructing_script_instance;
|
||||
|
||||
Ref<CSharpScript> script;
|
||||
Ref<MonoGCHandle> gchandle;
|
||||
|
||||
bool _reference_owner_unsafe();
|
||||
|
||||
/*
|
||||
* If true is returned, the caller must memdelete the script instance's owner.
|
||||
*/
|
||||
bool _unreference_owner_unsafe();
|
||||
|
||||
/*
|
||||
* If NULL is returned, the caller must destroy the script instance by removing it from its owner.
|
||||
*/
|
||||
MonoObject *_internal_new_managed();
|
||||
|
||||
// Do not use unless you know what you are doing
|
||||
friend void GDMonoInternals::tie_managed_to_unmanaged(MonoObject *, Object *);
|
||||
static CSharpInstance *create_for_managed_type(Object *p_owner, CSharpScript *p_script, const Ref<MonoGCHandle> &p_gchandle);
|
||||
|
||||
void _call_multilevel(MonoObject *p_mono_object, const StringName &p_method, const Variant **p_args, int p_argcount);
|
||||
|
||||
MultiplayerAPI::RPCMode _member_get_rpc_mode(IMonoClassMember *p_member) const;
|
||||
|
||||
void get_properties_state_for_reloading(List<Pair<StringName, Variant>> &r_state);
|
||||
|
||||
public:
|
||||
MonoObject *get_mono_object() const;
|
||||
|
||||
_FORCE_INLINE_ bool is_destructing_script_instance() { return destructing_script_instance; }
|
||||
|
||||
virtual Object *get_owner();
|
||||
|
||||
virtual bool set(const StringName &p_name, const Variant &p_value);
|
||||
virtual bool get(const StringName &p_name, Variant &r_ret) const;
|
||||
virtual void get_property_list(List<PropertyInfo> *p_properties) const;
|
||||
virtual Variant::Type get_property_type(const StringName &p_name, bool *r_is_valid) const;
|
||||
|
||||
virtual void get_method_list(List<MethodInfo> *p_list) const;
|
||||
virtual bool has_method(const StringName &p_method) const;
|
||||
virtual Variant call(const StringName &p_method, const Variant **p_args, int p_argcount, Variant::CallError &r_error);
|
||||
virtual void call_multilevel(const StringName &p_method, const Variant **p_args, int p_argcount);
|
||||
virtual void call_multilevel_reversed(const StringName &p_method, const Variant **p_args, int p_argcount);
|
||||
|
||||
void mono_object_disposed(MonoObject *p_obj);
|
||||
|
||||
/*
|
||||
* If 'r_delete_owner' is set to true, the caller must memdelete the script instance's owner. Otherwise, if
|
||||
* 'r_remove_script_instance' is set to true, the caller must destroy the script instance by removing it from its owner.
|
||||
*/
|
||||
void mono_object_disposed_baseref(MonoObject *p_obj, bool p_is_finalizer, bool &r_delete_owner, bool &r_remove_script_instance);
|
||||
|
||||
virtual void refcount_incremented();
|
||||
virtual bool refcount_decremented();
|
||||
|
||||
virtual MultiplayerAPI::RPCMode get_rpc_mode(const StringName &p_method) const;
|
||||
virtual MultiplayerAPI::RPCMode get_rset_mode(const StringName &p_variable) const;
|
||||
|
||||
virtual void notification(int p_notification);
|
||||
void _call_notification(int p_notification);
|
||||
|
||||
virtual String to_string(bool *r_valid);
|
||||
|
||||
virtual Ref<Script> get_script() const;
|
||||
|
||||
virtual ScriptLanguage *get_language();
|
||||
|
||||
CSharpInstance();
|
||||
~CSharpInstance();
|
||||
};
|
||||
|
||||
struct CSharpScriptBinding {
|
||||
bool inited;
|
||||
StringName type_name;
|
||||
GDMonoClass *wrapper_class;
|
||||
Ref<MonoGCHandle> gchandle;
|
||||
Object *owner;
|
||||
};
|
||||
|
||||
class CSharpLanguage : public ScriptLanguage {
|
||||
friend class CSharpScript;
|
||||
friend class CSharpInstance;
|
||||
|
||||
static CSharpLanguage *singleton;
|
||||
|
||||
bool finalizing;
|
||||
|
||||
GDMono *gdmono;
|
||||
SelfList<CSharpScript>::List script_list;
|
||||
|
||||
Mutex script_instances_mutex;
|
||||
Mutex script_gchandle_release_mutex;
|
||||
Mutex language_bind_mutex;
|
||||
|
||||
Map<Object *, CSharpScriptBinding> script_bindings;
|
||||
|
||||
#ifdef DEBUG_ENABLED
|
||||
// List of unsafe object references
|
||||
Map<ObjectID, int> unsafe_object_references;
|
||||
Mutex unsafe_object_references_lock;
|
||||
#endif
|
||||
|
||||
struct StringNameCache {
|
||||
StringName _signal_callback;
|
||||
StringName _set;
|
||||
StringName _get;
|
||||
StringName _get_property_list;
|
||||
StringName _notification;
|
||||
StringName _script_source;
|
||||
StringName dotctor; // .ctor
|
||||
StringName on_before_serialize; // OnBeforeSerialize
|
||||
StringName on_after_deserialize; // OnAfterDeserialize
|
||||
|
||||
StringNameCache();
|
||||
};
|
||||
|
||||
int lang_idx;
|
||||
|
||||
Dictionary scripts_metadata;
|
||||
bool scripts_metadata_invalidated;
|
||||
|
||||
// For debug_break and debug_break_parse
|
||||
int _debug_parse_err_line;
|
||||
String _debug_parse_err_file;
|
||||
String _debug_error;
|
||||
|
||||
void _load_scripts_metadata();
|
||||
|
||||
friend class GDMono;
|
||||
void _on_scripts_domain_unloaded();
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
EditorPlugin *godotsharp_editor;
|
||||
|
||||
static void _editor_init_callback();
|
||||
#endif
|
||||
|
||||
public:
|
||||
StringNameCache string_names;
|
||||
|
||||
Mutex &get_language_bind_mutex() { return language_bind_mutex; }
|
||||
|
||||
_FORCE_INLINE_ int get_language_index() { return lang_idx; }
|
||||
void set_language_index(int p_idx);
|
||||
|
||||
_FORCE_INLINE_ const StringNameCache &get_string_names() { return string_names; }
|
||||
|
||||
_FORCE_INLINE_ static CSharpLanguage *get_singleton() { return singleton; }
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
_FORCE_INLINE_ EditorPlugin *get_godotsharp_editor() const { return godotsharp_editor; }
|
||||
#endif
|
||||
|
||||
static void release_script_gchandle(Ref<MonoGCHandle> &p_gchandle);
|
||||
static void release_script_gchandle(MonoObject *p_expected_obj, Ref<MonoGCHandle> &p_gchandle);
|
||||
|
||||
bool debug_break(const String &p_error, bool p_allow_continue = true);
|
||||
bool debug_break_parse(const String &p_file, int p_line, const String &p_error);
|
||||
|
||||
#ifdef GD_MONO_HOT_RELOAD
|
||||
bool is_assembly_reloading_needed();
|
||||
void reload_assemblies(bool p_soft_reload);
|
||||
#endif
|
||||
|
||||
_FORCE_INLINE_ Dictionary get_scripts_metadata_or_nothing() {
|
||||
return scripts_metadata_invalidated ? Dictionary() : scripts_metadata;
|
||||
}
|
||||
|
||||
_FORCE_INLINE_ const Dictionary &get_scripts_metadata() {
|
||||
if (scripts_metadata_invalidated)
|
||||
_load_scripts_metadata();
|
||||
return scripts_metadata;
|
||||
}
|
||||
|
||||
virtual String get_name() const;
|
||||
|
||||
/* LANGUAGE FUNCTIONS */
|
||||
virtual String get_type() const;
|
||||
virtual String get_extension() const;
|
||||
virtual Error execute_file(const String &p_path);
|
||||
virtual void init();
|
||||
virtual void finish();
|
||||
|
||||
/* EDITOR FUNCTIONS */
|
||||
virtual void get_reserved_words(List<String> *p_words) const;
|
||||
virtual bool is_control_flow_keyword(String p_keyword) const;
|
||||
virtual void get_comment_delimiters(List<String> *p_delimiters) const;
|
||||
virtual void get_string_delimiters(List<String> *p_delimiters) const;
|
||||
virtual Ref<Script> get_template(const String &p_class_name, const String &p_base_class_name) const;
|
||||
virtual bool is_using_templates();
|
||||
virtual void make_template(const String &p_class_name, const String &p_base_class_name, Ref<Script> &p_script);
|
||||
/* TODO */ virtual bool validate(const String &p_script, int &r_line_error, int &r_col_error, String &r_test_error, const String &p_path, List<String> *r_functions, List<ScriptLanguage::Warning> *r_warnings = NULL, Set<int> *r_safe_lines = NULL) const { return true; }
|
||||
virtual String validate_path(const String &p_path) const;
|
||||
virtual Script *create_script() const;
|
||||
virtual bool has_named_classes() const;
|
||||
virtual bool supports_builtin_mode() const;
|
||||
/* TODO? */ virtual int find_function(const String &p_function, const String &p_code) const { return -1; }
|
||||
virtual String make_function(const String &p_class, const String &p_name, const PoolStringArray &p_args) const;
|
||||
virtual String _get_indentation() const;
|
||||
/* TODO? */ virtual void auto_indent_code(String &p_code, int p_from_line, int p_to_line) const {}
|
||||
/* TODO */ virtual void add_global_constant(const StringName &p_variable, const Variant &p_value) {}
|
||||
|
||||
/* DEBUGGER FUNCTIONS */
|
||||
virtual String debug_get_error() const;
|
||||
virtual int debug_get_stack_level_count() const;
|
||||
virtual int debug_get_stack_level_line(int p_level) const;
|
||||
virtual String debug_get_stack_level_function(int p_level) const;
|
||||
virtual String debug_get_stack_level_source(int p_level) const;
|
||||
/* TODO */ virtual void debug_get_stack_level_locals(int p_level, List<String> *p_locals, List<Variant> *p_values, int p_max_subitems, int p_max_depth) {}
|
||||
/* TODO */ virtual void debug_get_stack_level_members(int p_level, List<String> *p_members, List<Variant> *p_values, int p_max_subitems, int p_max_depth) {}
|
||||
/* TODO */ virtual void debug_get_globals(List<String> *p_locals, List<Variant> *p_values, int p_max_subitems, int p_max_depth) {}
|
||||
/* TODO */ virtual String debug_parse_stack_level_expression(int p_level, const String &p_expression, int p_max_subitems, int p_max_depth) { return ""; }
|
||||
virtual Vector<StackInfo> debug_get_current_stack_info();
|
||||
|
||||
/* PROFILING FUNCTIONS */
|
||||
/* TODO */ virtual void profiling_start() {}
|
||||
/* TODO */ virtual void profiling_stop() {}
|
||||
/* TODO */ virtual int profiling_get_accumulated_data(ProfilingInfo *p_info_arr, int p_info_max) { return 0; }
|
||||
/* TODO */ virtual int profiling_get_frame_data(ProfilingInfo *p_info_arr, int p_info_max) { return 0; }
|
||||
|
||||
virtual void frame();
|
||||
|
||||
/* TODO? */ virtual void get_public_functions(List<MethodInfo> *p_functions) const {}
|
||||
/* TODO? */ virtual void get_public_constants(List<Pair<String, Variant>> *p_constants) const {}
|
||||
|
||||
virtual void reload_all_scripts();
|
||||
virtual void reload_tool_script(const Ref<Script> &p_script, bool p_soft_reload);
|
||||
|
||||
/* LOADER FUNCTIONS */
|
||||
virtual void get_recognized_extensions(List<String> *p_extensions) const;
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
virtual Error open_in_external_editor(const Ref<Script> &p_script, int p_line, int p_col);
|
||||
virtual bool overrides_external_editor();
|
||||
#endif
|
||||
|
||||
/* THREAD ATTACHING */
|
||||
virtual void thread_enter();
|
||||
virtual void thread_exit();
|
||||
|
||||
// Don't use these. I'm watching you
|
||||
virtual void *alloc_instance_binding_data(Object *p_object);
|
||||
virtual void free_instance_binding_data(void *p_data);
|
||||
virtual void refcount_incremented_instance_binding(Object *p_object);
|
||||
virtual bool refcount_decremented_instance_binding(Object *p_object);
|
||||
|
||||
Map<Object *, CSharpScriptBinding>::Element *insert_script_binding(Object *p_object, const CSharpScriptBinding &p_script_binding);
|
||||
bool setup_csharp_script_binding(CSharpScriptBinding &r_script_binding, Object *p_object);
|
||||
|
||||
#ifdef DEBUG_ENABLED
|
||||
Vector<StackInfo> stack_trace_get_info(MonoObject *p_stack_trace);
|
||||
#endif
|
||||
|
||||
void post_unsafe_reference(Object *p_obj);
|
||||
void pre_unsafe_unreference(Object *p_obj);
|
||||
|
||||
CSharpLanguage();
|
||||
~CSharpLanguage();
|
||||
};
|
||||
|
||||
class ResourceFormatLoaderCSharpScript : public ResourceFormatLoader {
|
||||
public:
|
||||
virtual RES load(const String &p_path, const String &p_original_path = "", Error *r_error = NULL, bool p_no_subresource_cache = false);
|
||||
virtual void get_recognized_extensions(List<String> *p_extensions) const;
|
||||
virtual bool handles_type(const String &p_type) const;
|
||||
virtual String get_resource_type(const String &p_path) const;
|
||||
};
|
||||
|
||||
class ResourceFormatSaverCSharpScript : public ResourceFormatSaver {
|
||||
public:
|
||||
virtual Error save(const String &p_path, const RES &p_resource, uint32_t p_flags = 0);
|
||||
virtual void get_recognized_extensions(const RES &p_resource, List<String> *p_extensions) const;
|
||||
virtual bool recognize(const RES &p_resource) const;
|
||||
};
|
||||
|
||||
#endif // CSHARP_SCRIPT_H
|
24
doc_classes/CSharpScript.xml
Normal file
24
doc_classes/CSharpScript.xml
Normal file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<class name="CSharpScript" inherits="Script" version="3.6">
|
||||
<brief_description>
|
||||
A script implemented in the C# programming language (Mono-enabled builds only).
|
||||
</brief_description>
|
||||
<description>
|
||||
This class represents a C# script. It is the C# equivalent of the [GDScript] class and is only available in Mono-enabled Godot builds.
|
||||
See also [GodotSharp].
|
||||
</description>
|
||||
<tutorials>
|
||||
<link>$DOCS_URL/tutorials/scripting/c_sharp/index.html</link>
|
||||
</tutorials>
|
||||
<methods>
|
||||
<method name="new" qualifiers="vararg">
|
||||
<return type="Variant">
|
||||
</return>
|
||||
<description>
|
||||
Returns a new instance of the script.
|
||||
</description>
|
||||
</method>
|
||||
</methods>
|
||||
<constants>
|
||||
</constants>
|
||||
</class>
|
76
doc_classes/GodotSharp.xml
Normal file
76
doc_classes/GodotSharp.xml
Normal file
@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<class name="GodotSharp" inherits="Object" category="Core" version="3.6">
|
||||
<brief_description>
|
||||
Bridge between Godot and the Mono runtime (Mono-enabled builds only).
|
||||
</brief_description>
|
||||
<description>
|
||||
This class is a bridge between Godot and the Mono runtime. It exposes several low-level operations and is only available in Mono-enabled Godot builds.
|
||||
See also [CSharpScript].
|
||||
</description>
|
||||
<tutorials>
|
||||
</tutorials>
|
||||
<methods>
|
||||
<method name="attach_thread">
|
||||
<return type="void">
|
||||
</return>
|
||||
<description>
|
||||
Attaches the current thread to the Mono runtime.
|
||||
</description>
|
||||
</method>
|
||||
<method name="detach_thread">
|
||||
<return type="void">
|
||||
</return>
|
||||
<description>
|
||||
Detaches the current thread from the Mono runtime.
|
||||
</description>
|
||||
</method>
|
||||
<method name="get_domain_id">
|
||||
<return type="int">
|
||||
</return>
|
||||
<description>
|
||||
Returns the current MonoDomain ID.
|
||||
[b]Note:[/b] The Mono runtime must be initialized for this method to work (use [method is_runtime_initialized] to check). If the Mono runtime isn't initialized at the time this method is called, the engine will crash.
|
||||
</description>
|
||||
</method>
|
||||
<method name="get_scripts_domain_id">
|
||||
<return type="int">
|
||||
</return>
|
||||
<description>
|
||||
Returns the scripts MonoDomain's ID. This will be the same MonoDomain ID as [method get_domain_id], unless the scripts domain isn't loaded.
|
||||
[b]Note:[/b] The Mono runtime must be initialized for this method to work (use [method is_runtime_initialized] to check). If the Mono runtime isn't initialized at the time this method is called, the engine will crash.
|
||||
</description>
|
||||
</method>
|
||||
<method name="is_domain_finalizing_for_unload">
|
||||
<return type="bool">
|
||||
</return>
|
||||
<argument index="0" name="domain_id" type="int">
|
||||
</argument>
|
||||
<description>
|
||||
Returns [code]true[/code] if the domain is being finalized, [code]false[/code] otherwise.
|
||||
</description>
|
||||
</method>
|
||||
<method name="is_runtime_initialized">
|
||||
<return type="bool">
|
||||
</return>
|
||||
<description>
|
||||
Returns [code]true[/code] if the Mono runtime is initialized, [code]false[/code] otherwise.
|
||||
</description>
|
||||
</method>
|
||||
<method name="is_runtime_shutting_down">
|
||||
<return type="bool">
|
||||
</return>
|
||||
<description>
|
||||
Returns [code]true[/code] if the Mono runtime is shutting down, [code]false[/code] otherwise.
|
||||
</description>
|
||||
</method>
|
||||
<method name="is_scripts_domain_loaded">
|
||||
<return type="bool">
|
||||
</return>
|
||||
<description>
|
||||
Returns [code]true[/code] if the scripts domain is loaded, [code]false[/code] otherwise.
|
||||
</description>
|
||||
</method>
|
||||
</methods>
|
||||
<constants>
|
||||
</constants>
|
||||
</class>
|
16
editor/Godot.NET.Sdk/Godot.NET.Sdk.sln
Normal file
16
editor/Godot.NET.Sdk/Godot.NET.Sdk.sln
Normal file
@ -0,0 +1,16 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.NET.Sdk", "Godot.NET.Sdk\Godot.NET.Sdk.csproj", "{31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
35
editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj
Normal file
35
editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj
Normal file
@ -0,0 +1,35 @@
|
||||
<Project Sdk="Microsoft.Build.NoTargets/2.0.1">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
|
||||
<Description>MSBuild .NET Sdk for Godot projects.</Description>
|
||||
<Authors>Godot Engine contributors</Authors>
|
||||
|
||||
<PackageId>Godot.NET.Sdk</PackageId>
|
||||
<Version>3.3.0</Version>
|
||||
<PackageVersion>3.3.0</PackageVersion>
|
||||
<PackageProjectUrl>https://github.com/godotengine/godot/tree/master/modules/mono/editor/Godot.NET.Sdk</PackageProjectUrl>
|
||||
<PackageType>MSBuildSdk</PackageType>
|
||||
<PackageTags>MSBuildSdk</PackageTags>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<NuspecFile>Godot.NET.Sdk.nuspec</NuspecFile>
|
||||
<GenerateNuspecDependsOn>$(GenerateNuspecDependsOn);SetNuSpecProperties</GenerateNuspecDependsOn>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="SetNuSpecProperties" Condition=" Exists('$(NuspecFile)') ">
|
||||
<PropertyGroup>
|
||||
<NuspecProperties>
|
||||
id=$(PackageId);
|
||||
description=$(Description);
|
||||
authors=$(Authors);
|
||||
version=$(PackageVersion);
|
||||
packagetype=$(PackageType);
|
||||
tags=$(PackageTags);
|
||||
projecturl=$(PackageProjectUrl)
|
||||
</NuspecProperties>
|
||||
</PropertyGroup>
|
||||
</Target>
|
||||
</Project>
|
22
editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.nuspec
Normal file
22
editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.nuspec
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2011/10/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>$id$</id>
|
||||
<version>$version$</version>
|
||||
<description>$description$</description>
|
||||
<authors>$authors$</authors>
|
||||
<owners>$authors$</owners>
|
||||
<projectUrl>$projecturl$</projectUrl>
|
||||
<requireLicenseAcceptance>false</requireLicenseAcceptance>
|
||||
<license type="expression">MIT</license>
|
||||
<licenseUrl>https://licenses.nuget.org/MIT</licenseUrl>
|
||||
<tags>$tags$</tags>
|
||||
<packageTypes>
|
||||
<packageType name="$packagetype$" />
|
||||
</packageTypes>
|
||||
<repository url="$projecturl$" />
|
||||
</metadata>
|
||||
<files>
|
||||
<file src="Sdk\**" target="Sdk" />
|
||||
</files>
|
||||
</package>
|
117
editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props
Normal file
117
editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props
Normal file
@ -0,0 +1,117 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<!-- Determines if we should import Microsoft.NET.Sdk, if it wasn't already imported. -->
|
||||
<GodotSdkImportsMicrosoftNetSdk Condition=" '$(UsingMicrosoftNETSdk)' != 'true' ">true</GodotSdkImportsMicrosoftNetSdk>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<Configurations>Debug;ExportDebug;ExportRelease</Configurations>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
|
||||
<GodotProjectDir Condition=" '$(SolutionDir)' != '' ">$(SolutionDir)</GodotProjectDir>
|
||||
<GodotProjectDir Condition=" '$(SolutionDir)' == '' ">$(MSBuildProjectDirectory)</GodotProjectDir>
|
||||
<GodotProjectDir>$([MSBuild]::EnsureTrailingSlash('$(GodotProjectDir)'))</GodotProjectDir>
|
||||
|
||||
<!-- Custom output paths for Godot projects. In brief, 'bin\' and 'obj\' are moved to '$(GodotProjectDir)\.mono\temp\'. -->
|
||||
<BaseOutputPath>$(GodotProjectDir).mono\temp\bin\</BaseOutputPath>
|
||||
<OutputPath>$(GodotProjectDir).mono\temp\bin\$(Configuration)\</OutputPath>
|
||||
<!--
|
||||
Use custom IntermediateOutputPath and BaseIntermediateOutputPath only if it wasn't already set.
|
||||
Otherwise the old values may have already been changed by MSBuild which can cause problems with NuGet.
|
||||
-->
|
||||
<IntermediateOutputPath Condition=" '$(IntermediateOutputPath)' == '' ">$(GodotProjectDir).mono\temp\obj\$(Configuration)\</IntermediateOutputPath>
|
||||
<BaseIntermediateOutputPath Condition=" '$(BaseIntermediateOutputPath)' == '' ">$(GodotProjectDir).mono\temp\obj\</BaseIntermediateOutputPath>
|
||||
|
||||
<!-- Do not append the target framework name to the output path. -->
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Sdk="Microsoft.NET.Sdk" Project="Sdk.props" Condition=" '$(GodotSdkImportsMicrosoftNetSdk)' == 'true' " />
|
||||
|
||||
<PropertyGroup>
|
||||
<EnableDefaultNoneItems>false</EnableDefaultNoneItems>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
The Microsoft.NET.Sdk only understands of the Debug and Release configurations.
|
||||
We need to set the following properties manually for ExportDebug and ExportRelease.
|
||||
-->
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' or '$(Configuration)' == 'ExportDebug' ">
|
||||
<DebugSymbols Condition=" '$(DebugSymbols)' == '' ">true</DebugSymbols>
|
||||
<Optimize Condition=" '$(Optimize)' == '' ">false</Optimize>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'ExportRelease' ">
|
||||
<Optimize Condition=" '$(Optimize)' == '' ">true</Optimize>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<GodotApiConfiguration Condition=" '$(Configuration)' != 'ExportRelease' ">Debug</GodotApiConfiguration>
|
||||
<GodotApiConfiguration Condition=" '$(Configuration)' == 'ExportRelease' ">Release</GodotApiConfiguration>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Auto-detect the target Godot platform if it was not specified. -->
|
||||
<PropertyGroup Condition=" '$(GodotTargetPlatform)' == '' ">
|
||||
<GodotTargetPlatform Condition=" '$([MSBuild]::IsOsPlatform(Linux))' ">x11</GodotTargetPlatform>
|
||||
<GodotTargetPlatform Condition=" '$([MSBuild]::IsOsPlatform(FreeBSD))' ">x11</GodotTargetPlatform>
|
||||
<GodotTargetPlatform Condition=" '$([MSBuild]::IsOsPlatform(OSX))' ">osx</GodotTargetPlatform>
|
||||
<GodotTargetPlatform Condition=" '$([MSBuild]::IsOsPlatform(Windows))' ">windows</GodotTargetPlatform>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<GodotRealTIsDouble Condition=" '$(GodotRealTIsDouble)' == '' ">false</GodotRealTIsDouble>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Godot DefineConstants. -->
|
||||
<PropertyGroup>
|
||||
<!-- Define constant to identify Godot builds. -->
|
||||
<GodotDefineConstants>GODOT</GodotDefineConstants>
|
||||
|
||||
<!--
|
||||
Define constant to determine the target Godot platform. This includes the
|
||||
recognized platform names and the platform category (PC, MOBILE or WEB).
|
||||
-->
|
||||
<GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'windows' ">GODOT_WINDOWS;GODOT_PC</GodotPlatformConstants>
|
||||
<GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'x11' ">GODOT_X11;GODOT_PC</GodotPlatformConstants>
|
||||
<GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'osx' ">GODOT_OSX;GODOT_MACOS;GODOT_PC</GodotPlatformConstants>
|
||||
<GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'server' ">GODOT_SERVER;GODOT_PC</GodotPlatformConstants>
|
||||
<GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'uwp' ">GODOT_UWP;GODOT_PC</GodotPlatformConstants>
|
||||
<GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'haiku' ">GODOT_HAIKU;GODOT_PC</GodotPlatformConstants>
|
||||
<GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'android' ">GODOT_ANDROID;GODOT_MOBILE</GodotPlatformConstants>
|
||||
<GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'iphone' ">GODOT_IPHONE;GODOT_IOS;GODOT_MOBILE</GodotPlatformConstants>
|
||||
<GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'javascript' ">GODOT_JAVASCRIPT;GODOT_HTML5;GODOT_WASM;GODOT_WEB</GodotPlatformConstants>
|
||||
|
||||
<GodotDefineConstants>$(GodotDefineConstants);$(GodotPlatformConstants)</GodotDefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- ExportDebug also defines DEBUG like Debug does. -->
|
||||
<DefineConstants Condition=" '$(Configuration)' == 'ExportDebug' ">$(DefineConstants);DEBUG</DefineConstants>
|
||||
<!-- Debug defines TOOLS to differentiate between Debug and ExportDebug configurations. -->
|
||||
<DefineConstants Condition=" '$(Configuration)' == 'Debug' ">$(DefineConstants);TOOLS</DefineConstants>
|
||||
|
||||
<DefineConstants>$(GodotDefineConstants);$(DefineConstants)</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!--
|
||||
TODO:
|
||||
We should consider a nuget package for reference assemblies. This is difficult because the
|
||||
Godot scripting API is continuaslly breaking backwards compatibility even in patch releases.
|
||||
-->
|
||||
<Reference Include="GodotSharp">
|
||||
<Private>false</Private>
|
||||
<HintPath>$(GodotProjectDir).mono\assemblies\$(GodotApiConfiguration)\GodotSharp.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="GodotSharpEditor" Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<Private>false</Private>
|
||||
<HintPath>$(GodotProjectDir).mono\assemblies\$(GodotApiConfiguration)\GodotSharpEditor.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(AutomaticallyUseReferenceAssemblyPackages)' == '' and '$(MicrosoftNETFrameworkReferenceAssembliesLatestPackageVersion)' == '' ">
|
||||
<!-- Old 'Microsoft.NET.Sdk' so we reference the 'Microsoft.NETFramework.ReferenceAssemblies' package ourselves. -->
|
||||
<AutomaticallyUseReferenceAssemblyPackages>true</AutomaticallyUseReferenceAssemblyPackages>
|
||||
<MicrosoftNETFrameworkReferenceAssembliesLatestPackageVersion>1.0.0</MicrosoftNETFrameworkReferenceAssembliesLatestPackageVersion>
|
||||
<GodotUseNETFrameworkRefAssemblies>true</GodotUseNETFrameworkRefAssemblies>
|
||||
</PropertyGroup>
|
||||
</Project>
|
29
editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.targets
Normal file
29
editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.targets
Normal file
@ -0,0 +1,29 @@
|
||||
<Project>
|
||||
<Import Sdk="Microsoft.NET.Sdk" Project="Sdk.targets" Condition=" '$(GodotSdkImportsMicrosoftNetSdk)' == 'true' " />
|
||||
|
||||
<PropertyGroup>
|
||||
<!--
|
||||
Define constant to determine whether the real_t type in Godot is double precision or not.
|
||||
By default this is false, like the official Godot builds. If someone is using a custom
|
||||
Godot build where real_t is double, they can override the GodotRealTIsDouble property.
|
||||
-->
|
||||
<DefineConstants Condition=" '$(GodotRealTIsDouble)' == 'true' ">GODOT_REAL_T_IS_DOUBLE;$(DefineConstants)</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Backported from newer Microsoft.NET.Sdk version -->
|
||||
<Target Name="GodotIncludeTargetingPackReference" BeforeTargets="_GetRestoreSettingsPerFramework;_CheckForInvalidConfigurationAndPlatform"
|
||||
Condition=" '$(GodotUseNETFrameworkRefAssemblies)' == 'true' and '$(TargetFrameworkMoniker)' != '' and '$(TargetFrameworkIdentifier)' == '.NETFramework' and '$(AutomaticallyUseReferenceAssemblyPackages)' == 'true' ">
|
||||
<GetReferenceAssemblyPaths
|
||||
TargetFrameworkMoniker="$(TargetFrameworkMoniker)"
|
||||
RootPath="$(TargetFrameworkRootPath)"
|
||||
TargetFrameworkFallbackSearchPaths="$(TargetFrameworkFallbackSearchPaths)"
|
||||
BypassFrameworkInstallChecks="$(BypassFrameworkInstallChecks)"
|
||||
SuppressNotFoundError="true">
|
||||
<Output TaskParameter="FullFrameworkReferenceAssemblyPaths" PropertyName="_FullFrameworkReferenceAssemblyPaths"/>
|
||||
</GetReferenceAssemblyPaths>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="$(MicrosoftNETFrameworkReferenceAssembliesLatestPackageVersion)" IsImplicitlyDefined="true" Condition="'$(_FullFrameworkReferenceAssemblyPaths)' == ''"/>
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
</Project>
|
@ -0,0 +1,24 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Godot Engine contributors")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyDescriptionAttribute("MSBuild .NET Sdk for Godot projects.")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("3.3.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("3.3.0")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("Godot.NET.Sdk")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("Godot.NET.Sdk")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("3.3.0.0")]
|
||||
|
||||
// Generated by the MSBuild WriteCodeFragment class.
|
||||
|
@ -0,0 +1 @@
|
||||
7aa02142e9fcaa4587d162d62ea712ca0f3ade57
|
@ -0,0 +1,3 @@
|
||||
is_global = true
|
||||
build_property.RootNamespace = Godot.NET.Sdk
|
||||
build_property.ProjectDir = /home/relintai/Projects/godot/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/
|
355
editor/GodotTools/.gitignore
vendored
Normal file
355
editor/GodotTools/.gitignore
vendored
Normal file
@ -0,0 +1,355 @@
|
||||
# Rider
|
||||
.idea/
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/
|
||||
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding add-in
|
||||
.JustCode
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
175
editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs
Normal file
175
editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs
Normal file
@ -0,0 +1,175 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security;
|
||||
using Microsoft.Build.Framework;
|
||||
|
||||
namespace GodotTools.BuildLogger
|
||||
{
|
||||
public class GodotBuildLogger : ILogger
|
||||
{
|
||||
public static readonly string AssemblyPath = Path.GetFullPath(typeof(GodotBuildLogger).Assembly.Location);
|
||||
|
||||
public string Parameters { get; set; }
|
||||
public LoggerVerbosity Verbosity { get; set; }
|
||||
|
||||
private StreamWriter _logStreamWriter;
|
||||
private StreamWriter _issuesStreamWriter;
|
||||
private int _indent;
|
||||
|
||||
public void Initialize(IEventSource eventSource)
|
||||
{
|
||||
if (null == Parameters)
|
||||
throw new LoggerException("Log directory parameter not specified.");
|
||||
|
||||
string[] parameters = Parameters.Split(new[] { ';' });
|
||||
|
||||
string logDir = parameters[0];
|
||||
|
||||
if (string.IsNullOrEmpty(logDir))
|
||||
throw new LoggerException("Log directory parameter is empty.");
|
||||
|
||||
if (parameters.Length > 1)
|
||||
throw new LoggerException("Too many parameters passed.");
|
||||
|
||||
string logFile = Path.Combine(logDir, "msbuild_log.txt");
|
||||
string issuesFile = Path.Combine(logDir, "msbuild_issues.csv");
|
||||
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(logDir))
|
||||
Directory.CreateDirectory(logDir);
|
||||
|
||||
_logStreamWriter = new StreamWriter(logFile);
|
||||
_issuesStreamWriter = new StreamWriter(issuesFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex is UnauthorizedAccessException
|
||||
|| ex is ArgumentNullException
|
||||
|| ex is PathTooLongException
|
||||
|| ex is DirectoryNotFoundException
|
||||
|| ex is NotSupportedException
|
||||
|| ex is ArgumentException
|
||||
|| ex is SecurityException
|
||||
|| ex is IOException)
|
||||
{
|
||||
throw new LoggerException("Failed to create log file: " + ex.Message);
|
||||
}
|
||||
|
||||
// Unexpected failure
|
||||
throw;
|
||||
}
|
||||
|
||||
eventSource.ProjectStarted += eventSource_ProjectStarted;
|
||||
eventSource.ProjectFinished += eventSource_ProjectFinished;
|
||||
eventSource.MessageRaised += eventSource_MessageRaised;
|
||||
eventSource.WarningRaised += eventSource_WarningRaised;
|
||||
eventSource.ErrorRaised += eventSource_ErrorRaised;
|
||||
}
|
||||
|
||||
private void eventSource_ProjectStarted(object sender, ProjectStartedEventArgs e)
|
||||
{
|
||||
WriteLine(e.Message);
|
||||
_indent++;
|
||||
}
|
||||
|
||||
private void eventSource_ProjectFinished(object sender, ProjectFinishedEventArgs e)
|
||||
{
|
||||
_indent--;
|
||||
WriteLine(e.Message);
|
||||
}
|
||||
|
||||
private void eventSource_ErrorRaised(object sender, BuildErrorEventArgs e)
|
||||
{
|
||||
string line = $"{e.File}({e.LineNumber},{e.ColumnNumber}): error {e.Code}: {e.Message}";
|
||||
|
||||
if (!string.IsNullOrEmpty(e.ProjectFile))
|
||||
line += $" [{e.ProjectFile}]";
|
||||
|
||||
WriteLine(line);
|
||||
|
||||
string errorLine = $@"error,{e.File.CsvEscape()},{e.LineNumber},{e.ColumnNumber}," +
|
||||
$"{e.Code?.CsvEscape() ?? string.Empty},{e.Message.CsvEscape()}," +
|
||||
$"{e.ProjectFile?.CsvEscape() ?? string.Empty}";
|
||||
_issuesStreamWriter.WriteLine(errorLine);
|
||||
}
|
||||
|
||||
private void eventSource_WarningRaised(object sender, BuildWarningEventArgs e)
|
||||
{
|
||||
string line = $"{e.File}({e.LineNumber},{e.ColumnNumber}): warning {e.Code}: {e.Message}";
|
||||
|
||||
if (!string.IsNullOrEmpty(e.ProjectFile))
|
||||
line += $" [{e.ProjectFile}]";
|
||||
|
||||
WriteLine(line);
|
||||
|
||||
string warningLine = $@"warning,{e.File.CsvEscape()},{e.LineNumber},{e.ColumnNumber}," +
|
||||
$"{e.Code?.CsvEscape() ?? string.Empty},{e.Message.CsvEscape()}," +
|
||||
$"{e.ProjectFile?.CsvEscape() ?? string.Empty}";
|
||||
_issuesStreamWriter.WriteLine(warningLine);
|
||||
}
|
||||
|
||||
private void eventSource_MessageRaised(object sender, BuildMessageEventArgs e)
|
||||
{
|
||||
// BuildMessageEventArgs adds Importance to BuildEventArgs
|
||||
// Let's take account of the verbosity setting we've been passed in deciding whether to log the message
|
||||
if (e.Importance == MessageImportance.High && IsVerbosityAtLeast(LoggerVerbosity.Minimal)
|
||||
|| e.Importance == MessageImportance.Normal && IsVerbosityAtLeast(LoggerVerbosity.Normal)
|
||||
|| e.Importance == MessageImportance.Low && IsVerbosityAtLeast(LoggerVerbosity.Detailed))
|
||||
{
|
||||
WriteLineWithSenderAndMessage(string.Empty, e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a line to the log, adding the SenderName and Message
|
||||
/// (these parameters are on all MSBuild event argument objects)
|
||||
/// </summary>
|
||||
private void WriteLineWithSenderAndMessage(string line, BuildEventArgs e)
|
||||
{
|
||||
if (0 == string.Compare(e.SenderName, "MSBuild", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Well, if the sender name is MSBuild, let's leave it out for prettiness
|
||||
WriteLine(line + e.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteLine(e.SenderName + ": " + line + e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteLine(string line)
|
||||
{
|
||||
for (int i = _indent; i > 0; i--)
|
||||
{
|
||||
_logStreamWriter.Write("\t");
|
||||
}
|
||||
|
||||
_logStreamWriter.WriteLine(line);
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
_logStreamWriter.Close();
|
||||
_issuesStreamWriter.Close();
|
||||
}
|
||||
|
||||
private bool IsVerbosityAtLeast(LoggerVerbosity checkVerbosity)
|
||||
{
|
||||
return Verbosity >= checkVerbosity;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class StringExtensions
|
||||
{
|
||||
public static string CsvEscape(this string value, char delimiter = ',')
|
||||
{
|
||||
bool hasSpecialChar = value.IndexOfAny(new[] { '\"', '\n', '\r', delimiter }) != -1;
|
||||
|
||||
if (hasSpecialChar)
|
||||
return "\"" + value.Replace("\"", "\"\"") + "\"";
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<ProjectGuid>{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}</ProjectGuid>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>7.2</LangVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Build.Framework" Version="16.5.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
30
editor/GodotTools/GodotTools.Core/FileUtils.cs
Normal file
30
editor/GodotTools/GodotTools.Core/FileUtils.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using System.IO;
|
||||
|
||||
namespace GodotTools.Core
|
||||
{
|
||||
public static class FileUtils
|
||||
{
|
||||
public static void SaveBackupCopy(string filePath)
|
||||
{
|
||||
string backupPathBase = filePath + ".old";
|
||||
string backupPath = backupPathBase;
|
||||
|
||||
const int maxAttempts = 5;
|
||||
int attempt = 1;
|
||||
|
||||
while (File.Exists(backupPath) && attempt <= maxAttempts)
|
||||
{
|
||||
backupPath = backupPathBase + "." + (attempt);
|
||||
attempt++;
|
||||
}
|
||||
|
||||
if (attempt > maxAttempts + 1)
|
||||
{
|
||||
// Overwrite the oldest one
|
||||
backupPath = backupPathBase;
|
||||
}
|
||||
|
||||
File.Copy(filePath, backupPath, overwrite: true);
|
||||
}
|
||||
}
|
||||
}
|
7
editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj
Normal file
7
editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj
Normal file
@ -0,0 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<ProjectGuid>{639E48BD-44E5-4091-8EDD-22D36DC0768D}</ProjectGuid>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>7.2</LangVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
38
editor/GodotTools/GodotTools.Core/ProcessExtensions.cs
Normal file
38
editor/GodotTools/GodotTools.Core/ProcessExtensions.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GodotTools.Core
|
||||
{
|
||||
public static class ProcessExtensions
|
||||
{
|
||||
public static async Task WaitForExitAsync(this Process process, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
|
||||
void ProcessExited(object sender, EventArgs e)
|
||||
{
|
||||
tcs.TrySetResult(true);
|
||||
}
|
||||
|
||||
process.EnableRaisingEvents = true;
|
||||
process.Exited += ProcessExited;
|
||||
|
||||
try
|
||||
{
|
||||
if (process.HasExited)
|
||||
return;
|
||||
|
||||
using (cancellationToken.Register(() => tcs.TrySetCanceled()))
|
||||
{
|
||||
await tcs.Task;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
process.Exited -= ProcessExited;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
83
editor/GodotTools/GodotTools.Core/StringExtensions.cs
Normal file
83
editor/GodotTools/GodotTools.Core/StringExtensions.cs
Normal file
@ -0,0 +1,83 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace GodotTools.Core
|
||||
{
|
||||
public static class StringExtensions
|
||||
{
|
||||
private static readonly string _driveRoot = Path.GetPathRoot(Environment.CurrentDirectory);
|
||||
|
||||
public static string RelativeToPath(this string path, string dir)
|
||||
{
|
||||
// Make sure the directory ends with a path separator
|
||||
dir = Path.Combine(dir, " ").TrimEnd();
|
||||
|
||||
if (Path.DirectorySeparatorChar == '\\')
|
||||
dir = dir.Replace("/", "\\") + "\\";
|
||||
|
||||
var fullPath = new Uri(Path.GetFullPath(path), UriKind.Absolute);
|
||||
var relRoot = new Uri(Path.GetFullPath(dir), UriKind.Absolute);
|
||||
|
||||
// MakeRelativeUri converts spaces to %20, hence why we need UnescapeDataString
|
||||
return Uri.UnescapeDataString(relRoot.MakeRelativeUri(fullPath).ToString());
|
||||
}
|
||||
|
||||
public static string NormalizePath(this string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return path;
|
||||
|
||||
bool rooted = path.IsAbsolutePath();
|
||||
|
||||
path = path.Replace('\\', '/');
|
||||
path = path[path.Length - 1] == '/' ? path.Substring(0, path.Length - 1) : path;
|
||||
|
||||
string[] parts = path.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
path = string.Join(Path.DirectorySeparatorChar.ToString(), parts).Trim();
|
||||
|
||||
if (!rooted)
|
||||
return path;
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
string maybeDrive = parts[0];
|
||||
if (maybeDrive.Length == 2 && maybeDrive[1] == ':')
|
||||
return path; // Already has drive letter
|
||||
}
|
||||
|
||||
return Path.DirectorySeparatorChar + path;
|
||||
}
|
||||
|
||||
public static bool IsAbsolutePath(this string path)
|
||||
{
|
||||
return path.StartsWith("/", StringComparison.Ordinal) ||
|
||||
path.StartsWith("\\", StringComparison.Ordinal) ||
|
||||
path.StartsWith(_driveRoot, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public static string ToSafeDirName(this string dirName, bool allowDirSeparator = false)
|
||||
{
|
||||
var invalidChars = new List<string> {":", "*", "?", "\"", "<", ">", "|"};
|
||||
|
||||
if (allowDirSeparator)
|
||||
{
|
||||
// Directory separators are allowed, but disallow ".." to avoid going up the filesystem
|
||||
invalidChars.Add("..");
|
||||
}
|
||||
else
|
||||
{
|
||||
invalidChars.Add("/");
|
||||
}
|
||||
|
||||
string safeDirName = dirName.Replace("\\", "/").Trim();
|
||||
|
||||
foreach (string invalidChar in invalidChars)
|
||||
safeDirName = safeDirName.Replace(invalidChar, "-");
|
||||
|
||||
return safeDirName;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GodotTools.IdeMessaging.Utils;
|
||||
|
||||
namespace GodotTools.IdeMessaging.CLI
|
||||
{
|
||||
public class ForwarderMessageHandler : IMessageHandler
|
||||
{
|
||||
private readonly StreamWriter outputWriter;
|
||||
private readonly SemaphoreSlim outputWriteSem = new SemaphoreSlim(1);
|
||||
|
||||
public ForwarderMessageHandler(StreamWriter outputWriter)
|
||||
{
|
||||
this.outputWriter = outputWriter;
|
||||
}
|
||||
|
||||
public async Task<MessageContent> HandleRequest(Peer peer, string id, MessageContent content, ILogger logger)
|
||||
{
|
||||
await WriteRequestToOutput(id, content);
|
||||
return new MessageContent(MessageStatus.RequestNotSupported, "null");
|
||||
}
|
||||
|
||||
private async Task WriteRequestToOutput(string id, MessageContent content)
|
||||
{
|
||||
using (await outputWriteSem.UseAsync())
|
||||
{
|
||||
await outputWriter.WriteLineAsync("======= Request =======");
|
||||
await outputWriter.WriteLineAsync(id);
|
||||
await outputWriter.WriteLineAsync(content.Body.Count(c => c == '\n').ToString());
|
||||
await outputWriter.WriteLineAsync(content.Body);
|
||||
await outputWriter.WriteLineAsync("=======================");
|
||||
await outputWriter.FlushAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task WriteResponseToOutput(string id, MessageContent content)
|
||||
{
|
||||
using (await outputWriteSem.UseAsync())
|
||||
{
|
||||
await outputWriter.WriteLineAsync("======= Response =======");
|
||||
await outputWriter.WriteLineAsync(id);
|
||||
await outputWriter.WriteLineAsync(content.Body.Count(c => c == '\n').ToString());
|
||||
await outputWriter.WriteLineAsync(content.Body);
|
||||
await outputWriter.WriteLineAsync("========================");
|
||||
await outputWriter.FlushAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task WriteLineToOutput(string eventName)
|
||||
{
|
||||
using (await outputWriteSem.UseAsync())
|
||||
await outputWriter.WriteLineAsync($"======= {eventName} =======");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<ProjectGuid>{B06C2951-C8E3-4F28-80B2-717CF327EB19}</ProjectGuid>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net472</TargetFramework>
|
||||
<LangVersion>7.2</LangVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\GodotTools.IdeMessaging\GodotTools.IdeMessaging.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
217
editor/GodotTools/GodotTools.IdeMessaging.CLI/Program.cs
Normal file
217
editor/GodotTools/GodotTools.IdeMessaging.CLI/Program.cs
Normal file
@ -0,0 +1,217 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using GodotTools.IdeMessaging.Requests;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace GodotTools.IdeMessaging.CLI
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
private static readonly ILogger Logger = new CustomLogger();
|
||||
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
var mainTask = StartAsync(args, Console.OpenStandardInput(), Console.OpenStandardOutput());
|
||||
mainTask.Wait();
|
||||
return mainTask.Result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Unhandled exception: ", ex);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<int> StartAsync(string[] args, Stream inputStream, Stream outputStream)
|
||||
{
|
||||
var inputReader = new StreamReader(inputStream, Encoding.UTF8);
|
||||
var outputWriter = new StreamWriter(outputStream, Encoding.UTF8);
|
||||
|
||||
try
|
||||
{
|
||||
if (args.Length == 0)
|
||||
{
|
||||
Logger.LogError("Expected at least 1 argument");
|
||||
return 1;
|
||||
}
|
||||
|
||||
string godotProjectDir = args[0];
|
||||
|
||||
if (!Directory.Exists(godotProjectDir))
|
||||
{
|
||||
Logger.LogError($"The specified Godot project directory does not exist: {godotProjectDir}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var forwarder = new ForwarderMessageHandler(outputWriter);
|
||||
|
||||
using (var fwdClient = new Client("VisualStudioCode", godotProjectDir, forwarder, Logger))
|
||||
{
|
||||
fwdClient.Start();
|
||||
|
||||
// ReSharper disable AccessToDisposedClosure
|
||||
fwdClient.Connected += async () => await forwarder.WriteLineToOutput("Event=Connected");
|
||||
fwdClient.Disconnected += async () => await forwarder.WriteLineToOutput("Event=Disconnected");
|
||||
// ReSharper restore AccessToDisposedClosure
|
||||
|
||||
// TODO: Await connected with timeout
|
||||
|
||||
while (!fwdClient.IsDisposed)
|
||||
{
|
||||
string firstLine = await inputReader.ReadLineAsync();
|
||||
|
||||
if (firstLine == null || firstLine == "QUIT")
|
||||
goto ExitMainLoop;
|
||||
|
||||
string messageId = firstLine;
|
||||
|
||||
string messageArgcLine = await inputReader.ReadLineAsync();
|
||||
|
||||
if (messageArgcLine == null)
|
||||
{
|
||||
Logger.LogInfo("EOF when expecting argument count");
|
||||
goto ExitMainLoop;
|
||||
}
|
||||
|
||||
if (!int.TryParse(messageArgcLine, out int messageArgc))
|
||||
{
|
||||
Logger.LogError("Received invalid line for argument count: " + firstLine);
|
||||
continue;
|
||||
}
|
||||
|
||||
var body = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < messageArgc; i++)
|
||||
{
|
||||
string bodyLine = await inputReader.ReadLineAsync();
|
||||
|
||||
if (bodyLine == null)
|
||||
{
|
||||
Logger.LogInfo($"EOF when expecting body line #{i + 1}");
|
||||
goto ExitMainLoop;
|
||||
}
|
||||
|
||||
body.AppendLine(bodyLine);
|
||||
}
|
||||
|
||||
var response = await SendRequest(fwdClient, messageId, new MessageContent(MessageStatus.Ok, body.ToString()));
|
||||
|
||||
if (response == null)
|
||||
{
|
||||
Logger.LogError($"Failed to write message to the server: {messageId}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var content = new MessageContent(response.Status, JsonConvert.SerializeObject(response));
|
||||
await forwarder.WriteResponseToOutput(messageId, content);
|
||||
}
|
||||
}
|
||||
|
||||
ExitMainLoop:
|
||||
await forwarder.WriteLineToOutput("Event=Quit");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError("Unhandled exception", e);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<Response> SendRequest(Client client, string id, MessageContent content)
|
||||
{
|
||||
var handlers = new Dictionary<string, Func<Task<Response>>>
|
||||
{
|
||||
[PlayRequest.Id] = async () =>
|
||||
{
|
||||
var request = JsonConvert.DeserializeObject<PlayRequest>(content.Body);
|
||||
return await client.SendRequest<PlayResponse>(request);
|
||||
},
|
||||
[DebugPlayRequest.Id] = async () =>
|
||||
{
|
||||
var request = JsonConvert.DeserializeObject<DebugPlayRequest>(content.Body);
|
||||
return await client.SendRequest<DebugPlayResponse>(request);
|
||||
},
|
||||
[ReloadScriptsRequest.Id] = async () =>
|
||||
{
|
||||
var request = JsonConvert.DeserializeObject<ReloadScriptsRequest>(content.Body);
|
||||
return await client.SendRequest<ReloadScriptsResponse>(request);
|
||||
},
|
||||
[CodeCompletionRequest.Id] = async () =>
|
||||
{
|
||||
var request = JsonConvert.DeserializeObject<CodeCompletionRequest>(content.Body);
|
||||
return await client.SendRequest<CodeCompletionResponse>(request);
|
||||
}
|
||||
};
|
||||
|
||||
if (handlers.TryGetValue(id, out var handler))
|
||||
return await handler();
|
||||
|
||||
Console.WriteLine("INVALID REQUEST");
|
||||
return null;
|
||||
}
|
||||
|
||||
private class CustomLogger : ILogger
|
||||
{
|
||||
private static string ThisAppPath => Assembly.GetExecutingAssembly().Location;
|
||||
private static string ThisAppPathWithoutExtension => Path.ChangeExtension(ThisAppPath, null);
|
||||
|
||||
private static readonly string LogPath = $"{ThisAppPathWithoutExtension}.log";
|
||||
|
||||
private static StreamWriter NewWriter() => new StreamWriter(LogPath, append: true, encoding: Encoding.UTF8);
|
||||
|
||||
private static void Log(StreamWriter writer, string message)
|
||||
{
|
||||
writer.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}: {message}");
|
||||
}
|
||||
|
||||
public void LogDebug(string message)
|
||||
{
|
||||
using (var writer = NewWriter())
|
||||
{
|
||||
Log(writer, "DEBUG: " + message);
|
||||
}
|
||||
}
|
||||
|
||||
public void LogInfo(string message)
|
||||
{
|
||||
using (var writer = NewWriter())
|
||||
{
|
||||
Log(writer, "INFO: " + message);
|
||||
}
|
||||
}
|
||||
|
||||
public void LogWarning(string message)
|
||||
{
|
||||
using (var writer = NewWriter())
|
||||
{
|
||||
Log(writer, "WARN: " + message);
|
||||
}
|
||||
}
|
||||
|
||||
public void LogError(string message)
|
||||
{
|
||||
using (var writer = NewWriter())
|
||||
{
|
||||
Log(writer, "ERROR: " + message);
|
||||
}
|
||||
}
|
||||
|
||||
public void LogError(string message, Exception e)
|
||||
{
|
||||
using (var writer = NewWriter())
|
||||
{
|
||||
Log(writer, "EXCEPTION: " + message + '\n' + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
332
editor/GodotTools/GodotTools.IdeMessaging/Client.cs
Normal file
332
editor/GodotTools/GodotTools.IdeMessaging/Client.cs
Normal file
@ -0,0 +1,332 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using Newtonsoft.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GodotTools.IdeMessaging.Requests;
|
||||
using GodotTools.IdeMessaging.Utils;
|
||||
|
||||
namespace GodotTools.IdeMessaging
|
||||
{
|
||||
// ReSharper disable once UnusedType.Global
|
||||
public sealed class Client : IDisposable
|
||||
{
|
||||
private readonly ILogger logger;
|
||||
|
||||
private readonly string identity;
|
||||
|
||||
private string MetaFilePath { get; }
|
||||
private GodotIdeMetadata godotIdeMetadata;
|
||||
private readonly FileSystemWatcher fsWatcher;
|
||||
|
||||
private readonly IMessageHandler messageHandler;
|
||||
|
||||
private Peer peer;
|
||||
private readonly SemaphoreSlim connectionSem = new SemaphoreSlim(1);
|
||||
|
||||
private readonly Queue<NotifyAwaiter<bool>> clientConnectedAwaiters = new Queue<NotifyAwaiter<bool>>();
|
||||
private readonly Queue<NotifyAwaiter<bool>> clientDisconnectedAwaiters = new Queue<NotifyAwaiter<bool>>();
|
||||
|
||||
// ReSharper disable once UnusedMember.Global
|
||||
public async Task<bool> AwaitConnected()
|
||||
{
|
||||
var awaiter = new NotifyAwaiter<bool>();
|
||||
clientConnectedAwaiters.Enqueue(awaiter);
|
||||
return await awaiter;
|
||||
}
|
||||
|
||||
// ReSharper disable once UnusedMember.Global
|
||||
public async Task<bool> AwaitDisconnected()
|
||||
{
|
||||
var awaiter = new NotifyAwaiter<bool>();
|
||||
clientDisconnectedAwaiters.Enqueue(awaiter);
|
||||
return await awaiter;
|
||||
}
|
||||
|
||||
// ReSharper disable once MemberCanBePrivate.Global
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
// ReSharper disable once MemberCanBePrivate.Global
|
||||
public bool IsConnected => peer != null && !peer.IsDisposed && peer.IsTcpClientConnected;
|
||||
|
||||
// ReSharper disable once EventNeverSubscribedTo.Global
|
||||
public event Action Connected
|
||||
{
|
||||
add
|
||||
{
|
||||
if (peer != null && !peer.IsDisposed)
|
||||
peer.Connected += value;
|
||||
}
|
||||
remove
|
||||
{
|
||||
if (peer != null && !peer.IsDisposed)
|
||||
peer.Connected -= value;
|
||||
}
|
||||
}
|
||||
|
||||
// ReSharper disable once EventNeverSubscribedTo.Global
|
||||
public event Action Disconnected
|
||||
{
|
||||
add
|
||||
{
|
||||
if (peer != null && !peer.IsDisposed)
|
||||
peer.Disconnected += value;
|
||||
}
|
||||
remove
|
||||
{
|
||||
if (peer != null && !peer.IsDisposed)
|
||||
peer.Disconnected -= value;
|
||||
}
|
||||
}
|
||||
|
||||
~Client()
|
||||
{
|
||||
Dispose(disposing: false);
|
||||
}
|
||||
|
||||
public async void Dispose()
|
||||
{
|
||||
if (IsDisposed)
|
||||
return;
|
||||
|
||||
using (await connectionSem.UseAsync())
|
||||
{
|
||||
if (IsDisposed) // lock may not be fair
|
||||
return;
|
||||
IsDisposed = true;
|
||||
}
|
||||
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
peer?.Dispose();
|
||||
fsWatcher?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public Client(string identity, string godotProjectDir, IMessageHandler messageHandler, ILogger logger)
|
||||
{
|
||||
this.identity = identity;
|
||||
this.messageHandler = messageHandler;
|
||||
this.logger = logger;
|
||||
|
||||
string projectMetadataDir = Path.Combine(godotProjectDir, ".mono", "metadata");
|
||||
|
||||
MetaFilePath = Path.Combine(projectMetadataDir, GodotIdeMetadata.DefaultFileName);
|
||||
|
||||
// FileSystemWatcher requires an existing directory
|
||||
if (!File.Exists(projectMetadataDir))
|
||||
Directory.CreateDirectory(projectMetadataDir);
|
||||
|
||||
fsWatcher = new FileSystemWatcher(projectMetadataDir, GodotIdeMetadata.DefaultFileName);
|
||||
}
|
||||
|
||||
private async void OnMetaFileChanged(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
if (IsDisposed)
|
||||
return;
|
||||
|
||||
using (await connectionSem.UseAsync())
|
||||
{
|
||||
if (IsDisposed)
|
||||
return;
|
||||
|
||||
if (!File.Exists(MetaFilePath))
|
||||
return;
|
||||
|
||||
var metadata = ReadMetadataFile();
|
||||
|
||||
if (metadata != null && metadata != godotIdeMetadata)
|
||||
{
|
||||
godotIdeMetadata = metadata.Value;
|
||||
_ = Task.Run(ConnectToServer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnMetaFileDeleted(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
if (IsDisposed)
|
||||
return;
|
||||
|
||||
if (IsConnected)
|
||||
{
|
||||
using (await connectionSem.UseAsync())
|
||||
peer?.Dispose();
|
||||
}
|
||||
|
||||
// The file may have been re-created
|
||||
|
||||
using (await connectionSem.UseAsync())
|
||||
{
|
||||
if (IsDisposed)
|
||||
return;
|
||||
|
||||
if (IsConnected || !File.Exists(MetaFilePath))
|
||||
return;
|
||||
|
||||
var metadata = ReadMetadataFile();
|
||||
|
||||
if (metadata != null)
|
||||
{
|
||||
godotIdeMetadata = metadata.Value;
|
||||
_ = Task.Run(ConnectToServer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private GodotIdeMetadata? ReadMetadataFile()
|
||||
{
|
||||
using (var reader = File.OpenText(MetaFilePath))
|
||||
{
|
||||
string portStr = reader.ReadLine();
|
||||
|
||||
if (portStr == null)
|
||||
return null;
|
||||
|
||||
string editorExecutablePath = reader.ReadLine();
|
||||
|
||||
if (editorExecutablePath == null)
|
||||
return null;
|
||||
|
||||
if (!int.TryParse(portStr, out int port))
|
||||
return null;
|
||||
|
||||
return new GodotIdeMetadata(port, editorExecutablePath);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AcceptClient(TcpClient tcpClient)
|
||||
{
|
||||
logger.LogDebug("Accept client...");
|
||||
|
||||
using (peer = new Peer(tcpClient, new ClientHandshake(), messageHandler, logger))
|
||||
{
|
||||
// ReSharper disable AccessToDisposedClosure
|
||||
peer.Connected += () =>
|
||||
{
|
||||
logger.LogInfo("Connection open with Ide Client");
|
||||
|
||||
while (clientConnectedAwaiters.Count > 0)
|
||||
clientConnectedAwaiters.Dequeue().SetResult(true);
|
||||
};
|
||||
|
||||
peer.Disconnected += () =>
|
||||
{
|
||||
while (clientDisconnectedAwaiters.Count > 0)
|
||||
clientDisconnectedAwaiters.Dequeue().SetResult(true);
|
||||
};
|
||||
// ReSharper restore AccessToDisposedClosure
|
||||
|
||||
try
|
||||
{
|
||||
if (!await peer.DoHandshake(identity))
|
||||
{
|
||||
logger.LogError("Handshake failed");
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError("Handshake failed with unhandled exception: ", e);
|
||||
return;
|
||||
}
|
||||
|
||||
await peer.Process();
|
||||
|
||||
logger.LogInfo("Connection closed with Ide Client");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ConnectToServer()
|
||||
{
|
||||
var tcpClient = new TcpClient();
|
||||
|
||||
try
|
||||
{
|
||||
logger.LogInfo("Connecting to Godot Ide Server");
|
||||
|
||||
await tcpClient.ConnectAsync(IPAddress.Loopback, godotIdeMetadata.Port);
|
||||
|
||||
logger.LogInfo("Connection open with Godot Ide Server");
|
||||
|
||||
await AcceptClient(tcpClient);
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
if (e.SocketErrorCode == SocketError.ConnectionRefused)
|
||||
logger.LogError("The connection to the Godot Ide Server was refused");
|
||||
else
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// ReSharper disable once UnusedMember.Global
|
||||
public async void Start()
|
||||
{
|
||||
fsWatcher.Changed += OnMetaFileChanged;
|
||||
fsWatcher.Deleted += OnMetaFileDeleted;
|
||||
fsWatcher.EnableRaisingEvents = true;
|
||||
|
||||
using (await connectionSem.UseAsync())
|
||||
{
|
||||
if (IsDisposed)
|
||||
return;
|
||||
|
||||
if (IsConnected)
|
||||
return;
|
||||
|
||||
if (!File.Exists(MetaFilePath))
|
||||
{
|
||||
logger.LogInfo("There is no Godot Ide Server running");
|
||||
return;
|
||||
}
|
||||
|
||||
var metadata = ReadMetadataFile();
|
||||
|
||||
if (metadata != null)
|
||||
{
|
||||
godotIdeMetadata = metadata.Value;
|
||||
_ = Task.Run(ConnectToServer);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogError("Failed to read Godot Ide metadata file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<TResponse> SendRequest<TResponse>(Request request)
|
||||
where TResponse : Response, new()
|
||||
{
|
||||
if (!IsConnected)
|
||||
{
|
||||
logger.LogError("Cannot write request. Not connected to the Godot Ide Server.");
|
||||
return null;
|
||||
}
|
||||
|
||||
string body = JsonConvert.SerializeObject(request);
|
||||
return await peer.SendRequest<TResponse>(request.Id, body);
|
||||
}
|
||||
|
||||
public async Task<TResponse> SendRequest<TResponse>(string id, string body)
|
||||
where TResponse : Response, new()
|
||||
{
|
||||
if (!IsConnected)
|
||||
{
|
||||
logger.LogError("Cannot write request. Not connected to the Godot Ide Server.");
|
||||
return null;
|
||||
}
|
||||
|
||||
return await peer.SendRequest<TResponse>(id, body);
|
||||
}
|
||||
}
|
||||
}
|
44
editor/GodotTools/GodotTools.IdeMessaging/ClientHandshake.cs
Normal file
44
editor/GodotTools/GodotTools.IdeMessaging/ClientHandshake.cs
Normal file
@ -0,0 +1,44 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace GodotTools.IdeMessaging
|
||||
{
|
||||
public class ClientHandshake : IHandshake
|
||||
{
|
||||
private static readonly string ClientHandshakeBase = $"{Peer.ClientHandshakeName},Version={Peer.ProtocolVersionMajor}.{Peer.ProtocolVersionMinor}.{Peer.ProtocolVersionRevision}";
|
||||
private static readonly string ServerHandshakePattern = $@"{Regex.Escape(Peer.ServerHandshakeName)},Version=([0-9]+)\.([0-9]+)\.([0-9]+),([_a-zA-Z][_a-zA-Z0-9]{{0,63}})";
|
||||
|
||||
public string GetHandshakeLine(string identity) => $"{ClientHandshakeBase},{identity}";
|
||||
|
||||
public bool IsValidPeerHandshake(string handshake, out string identity, ILogger logger)
|
||||
{
|
||||
identity = null;
|
||||
|
||||
var match = Regex.Match(handshake, ServerHandshakePattern);
|
||||
|
||||
if (!match.Success)
|
||||
return false;
|
||||
|
||||
if (!uint.TryParse(match.Groups[1].Value, out uint serverMajor) || Peer.ProtocolVersionMajor != serverMajor)
|
||||
{
|
||||
logger.LogDebug("Incompatible major version: " + match.Groups[1].Value);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!uint.TryParse(match.Groups[2].Value, out uint serverMinor) || Peer.ProtocolVersionMinor < serverMinor)
|
||||
{
|
||||
logger.LogDebug("Incompatible minor version: " + match.Groups[2].Value);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!uint.TryParse(match.Groups[3].Value, out uint _)) // Revision
|
||||
{
|
||||
logger.LogDebug("Incompatible revision build: " + match.Groups[3].Value);
|
||||
return false;
|
||||
}
|
||||
|
||||
identity = match.Groups[4].Value;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using GodotTools.IdeMessaging.Requests;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace GodotTools.IdeMessaging
|
||||
{
|
||||
// ReSharper disable once UnusedType.Global
|
||||
public abstract class ClientMessageHandler : IMessageHandler
|
||||
{
|
||||
private readonly Dictionary<string, Peer.RequestHandler> requestHandlers;
|
||||
|
||||
protected ClientMessageHandler()
|
||||
{
|
||||
requestHandlers = InitializeRequestHandlers();
|
||||
}
|
||||
|
||||
public async Task<MessageContent> HandleRequest(Peer peer, string id, MessageContent content, ILogger logger)
|
||||
{
|
||||
if (!requestHandlers.TryGetValue(id, out var handler))
|
||||
{
|
||||
logger.LogError($"Received unknown request: {id}");
|
||||
return new MessageContent(MessageStatus.RequestNotSupported, "null");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var response = await handler(peer, content);
|
||||
return new MessageContent(response.Status, JsonConvert.SerializeObject(response));
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
logger.LogError($"Received request with invalid body: {id}");
|
||||
return new MessageContent(MessageStatus.InvalidRequestBody, "null");
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, Peer.RequestHandler> InitializeRequestHandlers()
|
||||
{
|
||||
return new Dictionary<string, Peer.RequestHandler>
|
||||
{
|
||||
[OpenFileRequest.Id] = async (peer, content) =>
|
||||
{
|
||||
var request = JsonConvert.DeserializeObject<OpenFileRequest>(content.Body);
|
||||
return await HandleOpenFile(request);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected abstract Task<Response> HandleOpenFile(OpenFileRequest request);
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
namespace GodotTools.IdeMessaging
|
||||
{
|
||||
public readonly struct GodotIdeMetadata
|
||||
{
|
||||
public int Port { get; }
|
||||
public string EditorExecutablePath { get; }
|
||||
|
||||
public const string DefaultFileName = "ide_messaging_meta.txt";
|
||||
|
||||
public GodotIdeMetadata(int port, string editorExecutablePath)
|
||||
{
|
||||
Port = port;
|
||||
EditorExecutablePath = editorExecutablePath;
|
||||
}
|
||||
|
||||
public static bool operator ==(GodotIdeMetadata a, GodotIdeMetadata b)
|
||||
{
|
||||
return a.Port == b.Port && a.EditorExecutablePath == b.EditorExecutablePath;
|
||||
}
|
||||
|
||||
public static bool operator !=(GodotIdeMetadata a, GodotIdeMetadata b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj is GodotIdeMetadata metadata)
|
||||
return metadata == this;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool Equals(GodotIdeMetadata other)
|
||||
{
|
||||
return Port == other.Port && EditorExecutablePath == other.EditorExecutablePath;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
return (Port * 397) ^ (EditorExecutablePath != null ? EditorExecutablePath.GetHashCode() : 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<ProjectGuid>{92600954-25F0-4291-8E11-1FEE9FC4BE20}</ProjectGuid>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>7.2</LangVersion>
|
||||
<PackageId>GodotTools.IdeMessaging</PackageId>
|
||||
<Version>1.1.0</Version>
|
||||
<AssemblyVersion>$(Version)</AssemblyVersion>
|
||||
<Authors>Godot Engine contributors</Authors>
|
||||
<Company />
|
||||
<PackageTags>godot</PackageTags>
|
||||
<RepositoryUrl>https://github.com/godotengine/godot/tree/master/modules/mono/editor/GodotTools/GodotTools.IdeMessaging</RepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<Description>
|
||||
This library enables communication with the Godot Engine editor (the version with .NET support).
|
||||
It's intended for use in IDEs/editors plugins for a better experience working with Godot C# projects.
|
||||
|
||||
A client using this library is only compatible with servers of the same major version and of a lower or equal minor version.
|
||||
</Description>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
8
editor/GodotTools/GodotTools.IdeMessaging/IHandshake.cs
Normal file
8
editor/GodotTools/GodotTools.IdeMessaging/IHandshake.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace GodotTools.IdeMessaging
|
||||
{
|
||||
public interface IHandshake
|
||||
{
|
||||
string GetHandshakeLine(string identity);
|
||||
bool IsValidPeerHandshake(string handshake, out string identity, ILogger logger);
|
||||
}
|
||||
}
|
13
editor/GodotTools/GodotTools.IdeMessaging/ILogger.cs
Normal file
13
editor/GodotTools/GodotTools.IdeMessaging/ILogger.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
namespace GodotTools.IdeMessaging
|
||||
{
|
||||
public interface ILogger
|
||||
{
|
||||
void LogDebug(string message);
|
||||
void LogInfo(string message);
|
||||
void LogWarning(string message);
|
||||
void LogError(string message);
|
||||
void LogError(string message, Exception e);
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GodotTools.IdeMessaging
|
||||
{
|
||||
public interface IMessageHandler
|
||||
{
|
||||
Task<MessageContent> HandleRequest(Peer peer, string id, MessageContent content, ILogger logger);
|
||||
}
|
||||
}
|
52
editor/GodotTools/GodotTools.IdeMessaging/Message.cs
Normal file
52
editor/GodotTools/GodotTools.IdeMessaging/Message.cs
Normal file
@ -0,0 +1,52 @@
|
||||
namespace GodotTools.IdeMessaging
|
||||
{
|
||||
public class Message
|
||||
{
|
||||
public MessageKind Kind { get; }
|
||||
public string Id { get; }
|
||||
public MessageContent Content { get; }
|
||||
|
||||
public Message(MessageKind kind, string id, MessageContent content)
|
||||
{
|
||||
Kind = kind;
|
||||
Id = id;
|
||||
Content = content;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Kind} | {Id}";
|
||||
}
|
||||
}
|
||||
|
||||
public enum MessageKind
|
||||
{
|
||||
Request,
|
||||
Response
|
||||
}
|
||||
|
||||
public enum MessageStatus
|
||||
{
|
||||
Ok,
|
||||
RequestNotSupported,
|
||||
InvalidRequestBody
|
||||
}
|
||||
|
||||
public readonly struct MessageContent
|
||||
{
|
||||
public MessageStatus Status { get; }
|
||||
public string Body { get; }
|
||||
|
||||
public MessageContent(string body)
|
||||
{
|
||||
Status = MessageStatus.Ok;
|
||||
Body = body;
|
||||
}
|
||||
|
||||
public MessageContent(MessageStatus status, string body)
|
||||
{
|
||||
Status = status;
|
||||
Body = body;
|
||||
}
|
||||
}
|
||||
}
|
100
editor/GodotTools/GodotTools.IdeMessaging/MessageDecoder.cs
Normal file
100
editor/GodotTools/GodotTools.IdeMessaging/MessageDecoder.cs
Normal file
@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace GodotTools.IdeMessaging
|
||||
{
|
||||
public class MessageDecoder
|
||||
{
|
||||
private class DecodedMessage
|
||||
{
|
||||
public MessageKind? Kind;
|
||||
public string Id;
|
||||
public MessageStatus? Status;
|
||||
public readonly StringBuilder Body = new StringBuilder();
|
||||
public uint? PendingBodyLines;
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Kind = null;
|
||||
Id = null;
|
||||
Status = null;
|
||||
Body.Clear();
|
||||
PendingBodyLines = null;
|
||||
}
|
||||
|
||||
public Message ToMessage()
|
||||
{
|
||||
if (!Kind.HasValue || Id == null || !Status.HasValue ||
|
||||
!PendingBodyLines.HasValue || PendingBodyLines.Value > 0)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
return new Message(Kind.Value, Id, new MessageContent(Status.Value, Body.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
public enum State
|
||||
{
|
||||
Decoding,
|
||||
Decoded,
|
||||
Errored
|
||||
}
|
||||
|
||||
private readonly DecodedMessage decodingMessage = new DecodedMessage();
|
||||
|
||||
public State Decode(string messageLine, out Message decodedMessage)
|
||||
{
|
||||
decodedMessage = null;
|
||||
|
||||
if (!decodingMessage.Kind.HasValue)
|
||||
{
|
||||
if (!Enum.TryParse(messageLine, ignoreCase: true, out MessageKind kind))
|
||||
{
|
||||
decodingMessage.Clear();
|
||||
return State.Errored;
|
||||
}
|
||||
|
||||
decodingMessage.Kind = kind;
|
||||
}
|
||||
else if (decodingMessage.Id == null)
|
||||
{
|
||||
decodingMessage.Id = messageLine;
|
||||
}
|
||||
else if (decodingMessage.Status == null)
|
||||
{
|
||||
if (!Enum.TryParse(messageLine, ignoreCase: true, out MessageStatus status))
|
||||
{
|
||||
decodingMessage.Clear();
|
||||
return State.Errored;
|
||||
}
|
||||
|
||||
decodingMessage.Status = status;
|
||||
}
|
||||
else if (decodingMessage.PendingBodyLines == null)
|
||||
{
|
||||
if (!uint.TryParse(messageLine, out uint pendingBodyLines))
|
||||
{
|
||||
decodingMessage.Clear();
|
||||
return State.Errored;
|
||||
}
|
||||
|
||||
decodingMessage.PendingBodyLines = pendingBodyLines;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (decodingMessage.PendingBodyLines > 0)
|
||||
{
|
||||
decodingMessage.Body.AppendLine(messageLine);
|
||||
decodingMessage.PendingBodyLines -= 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
decodedMessage = decodingMessage.ToMessage();
|
||||
decodingMessage.Clear();
|
||||
return State.Decoded;
|
||||
}
|
||||
}
|
||||
|
||||
return State.Decoding;
|
||||
}
|
||||
}
|
||||
}
|
302
editor/GodotTools/GodotTools.IdeMessaging/Peer.cs
Normal file
302
editor/GodotTools/GodotTools.IdeMessaging/Peer.cs
Normal file
@ -0,0 +1,302 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Sockets;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GodotTools.IdeMessaging.Requests;
|
||||
using GodotTools.IdeMessaging.Utils;
|
||||
|
||||
namespace GodotTools.IdeMessaging
|
||||
{
|
||||
public sealed class Peer : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Major version.
|
||||
/// There is no forward nor backward compatibility between different major versions.
|
||||
/// Connection is refused if client and server have different major versions.
|
||||
/// </summary>
|
||||
public static readonly int ProtocolVersionMajor = Assembly.GetAssembly(typeof(Peer)).GetName().Version.Major;
|
||||
|
||||
/// <summary>
|
||||
/// Minor version, which clients must be backward compatible with.
|
||||
/// Connection is refused if the client's minor version is lower than the server's.
|
||||
/// </summary>
|
||||
public static readonly int ProtocolVersionMinor = Assembly.GetAssembly(typeof(Peer)).GetName().Version.Minor;
|
||||
|
||||
/// <summary>
|
||||
/// Revision, which doesn't affect compatibility.
|
||||
/// </summary>
|
||||
public static readonly int ProtocolVersionRevision = Assembly.GetAssembly(typeof(Peer)).GetName().Version.Revision;
|
||||
|
||||
public const string ClientHandshakeName = "GodotIdeClient";
|
||||
public const string ServerHandshakeName = "GodotIdeServer";
|
||||
|
||||
private const int ClientWriteTimeout = 8000;
|
||||
|
||||
public delegate Task<Response> RequestHandler(Peer peer, MessageContent content);
|
||||
|
||||
private readonly TcpClient tcpClient;
|
||||
|
||||
private readonly TextReader clientReader;
|
||||
private readonly TextWriter clientWriter;
|
||||
|
||||
private readonly SemaphoreSlim writeSem = new SemaphoreSlim(1);
|
||||
|
||||
private string remoteIdentity = string.Empty;
|
||||
public string RemoteIdentity => remoteIdentity;
|
||||
|
||||
public event Action Connected;
|
||||
public event Action Disconnected;
|
||||
|
||||
private ILogger Logger { get; }
|
||||
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
public bool IsTcpClientConnected => tcpClient.Client != null && tcpClient.Client.Connected;
|
||||
|
||||
private bool IsConnected { get; set; }
|
||||
|
||||
private readonly IHandshake handshake;
|
||||
private readonly IMessageHandler messageHandler;
|
||||
|
||||
private readonly Dictionary<string, Queue<ResponseAwaiter>> requestAwaiterQueues = new Dictionary<string, Queue<ResponseAwaiter>>();
|
||||
private readonly SemaphoreSlim requestsSem = new SemaphoreSlim(1);
|
||||
|
||||
public Peer(TcpClient tcpClient, IHandshake handshake, IMessageHandler messageHandler, ILogger logger)
|
||||
{
|
||||
this.tcpClient = tcpClient;
|
||||
this.handshake = handshake;
|
||||
this.messageHandler = messageHandler;
|
||||
|
||||
Logger = logger;
|
||||
|
||||
NetworkStream clientStream = tcpClient.GetStream();
|
||||
clientStream.WriteTimeout = ClientWriteTimeout;
|
||||
|
||||
clientReader = new StreamReader(clientStream, Encoding.UTF8);
|
||||
clientWriter = new StreamWriter(clientStream, Encoding.UTF8) {NewLine = "\n"};
|
||||
}
|
||||
|
||||
public async Task Process()
|
||||
{
|
||||
try
|
||||
{
|
||||
var decoder = new MessageDecoder();
|
||||
|
||||
string messageLine;
|
||||
while ((messageLine = await ReadLine()) != null)
|
||||
{
|
||||
var state = decoder.Decode(messageLine, out var msg);
|
||||
|
||||
if (state == MessageDecoder.State.Decoding)
|
||||
continue; // Not finished decoding yet
|
||||
|
||||
if (state == MessageDecoder.State.Errored)
|
||||
{
|
||||
Logger.LogError($"Received message line with invalid format: {messageLine}");
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger.LogDebug($"Received message: {msg}");
|
||||
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
if (msg.Kind == MessageKind.Request)
|
||||
{
|
||||
var responseContent = await messageHandler.HandleRequest(this, msg.Id, msg.Content, Logger);
|
||||
await WriteMessage(new Message(MessageKind.Response, msg.Id, responseContent));
|
||||
}
|
||||
else if (msg.Kind == MessageKind.Response)
|
||||
{
|
||||
ResponseAwaiter responseAwaiter;
|
||||
|
||||
using (await requestsSem.UseAsync())
|
||||
{
|
||||
if (!requestAwaiterQueues.TryGetValue(msg.Id, out var queue) || queue.Count <= 0)
|
||||
{
|
||||
Logger.LogError($"Received unexpected response: {msg.Id}");
|
||||
return;
|
||||
}
|
||||
|
||||
responseAwaiter = queue.Dequeue();
|
||||
}
|
||||
|
||||
responseAwaiter.SetResult(msg.Content);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IndexOutOfRangeException($"Invalid message kind {msg.Kind}");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError($"Message handler for '{msg}' failed with exception", e);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError($"Exception thrown from message handler. Message: {msg}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError("Unhandled exception in the peer loop", e);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> DoHandshake(string identity)
|
||||
{
|
||||
if (!await WriteLine(handshake.GetHandshakeLine(identity)))
|
||||
{
|
||||
Logger.LogError("Could not write handshake");
|
||||
return false;
|
||||
}
|
||||
|
||||
var readHandshakeTask = ReadLine();
|
||||
|
||||
if (await Task.WhenAny(readHandshakeTask, Task.Delay(8000)) != readHandshakeTask)
|
||||
{
|
||||
Logger.LogError("Timeout waiting for the client handshake");
|
||||
return false;
|
||||
}
|
||||
|
||||
string peerHandshake = await readHandshakeTask;
|
||||
|
||||
if (handshake == null || !handshake.IsValidPeerHandshake(peerHandshake, out remoteIdentity, Logger))
|
||||
{
|
||||
Logger.LogError("Received invalid handshake: " + peerHandshake);
|
||||
return false;
|
||||
}
|
||||
|
||||
IsConnected = true;
|
||||
Connected?.Invoke();
|
||||
|
||||
Logger.LogInfo("Peer connection started");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<string> ReadLine()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await clientReader.ReadLineAsync();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (IsDisposed)
|
||||
{
|
||||
var se = e as SocketException ?? e.InnerException as SocketException;
|
||||
if (se != null && se.SocketErrorCode == SocketError.Interrupted)
|
||||
return null;
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private Task<bool> WriteMessage(Message message)
|
||||
{
|
||||
Logger.LogDebug($"Sending message: {message}");
|
||||
int bodyLineCount = message.Content.Body.Count(c => c == '\n');
|
||||
|
||||
bodyLineCount += 1; // Extra line break at the end
|
||||
|
||||
var builder = new StringBuilder();
|
||||
|
||||
builder.AppendLine(message.Kind.ToString());
|
||||
builder.AppendLine(message.Id);
|
||||
builder.AppendLine(message.Content.Status.ToString());
|
||||
builder.AppendLine(bodyLineCount.ToString());
|
||||
builder.AppendLine(message.Content.Body);
|
||||
|
||||
return WriteLine(builder.ToString());
|
||||
}
|
||||
|
||||
public async Task<TResponse> SendRequest<TResponse>(string id, string body)
|
||||
where TResponse : Response, new()
|
||||
{
|
||||
ResponseAwaiter responseAwaiter;
|
||||
|
||||
using (await requestsSem.UseAsync())
|
||||
{
|
||||
bool written = await WriteMessage(new Message(MessageKind.Request, id, new MessageContent(body)));
|
||||
|
||||
if (!written)
|
||||
return null;
|
||||
|
||||
if (!requestAwaiterQueues.TryGetValue(id, out var queue))
|
||||
{
|
||||
queue = new Queue<ResponseAwaiter>();
|
||||
requestAwaiterQueues.Add(id, queue);
|
||||
}
|
||||
|
||||
responseAwaiter = new ResponseAwaiter<TResponse>();
|
||||
queue.Enqueue(responseAwaiter);
|
||||
}
|
||||
|
||||
return (TResponse)await responseAwaiter;
|
||||
}
|
||||
|
||||
private async Task<bool> WriteLine(string text)
|
||||
{
|
||||
if (clientWriter == null || IsDisposed || !IsTcpClientConnected)
|
||||
return false;
|
||||
|
||||
using (await writeSem.UseAsync())
|
||||
{
|
||||
try
|
||||
{
|
||||
await clientWriter.WriteLineAsync(text);
|
||||
await clientWriter.FlushAsync();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
{
|
||||
var se = e as SocketException ?? e.InnerException as SocketException;
|
||||
if (se != null && se.SocketErrorCode == SocketError.Shutdown)
|
||||
Logger.LogInfo("Client disconnected ungracefully");
|
||||
else
|
||||
Logger.LogError("Exception thrown when trying to write to client", e);
|
||||
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ReSharper disable once UnusedMember.Global
|
||||
public void ShutdownSocketSend()
|
||||
{
|
||||
tcpClient.Client.Shutdown(SocketShutdown.Send);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (IsDisposed)
|
||||
return;
|
||||
|
||||
IsDisposed = true;
|
||||
|
||||
if (IsTcpClientConnected)
|
||||
{
|
||||
if (IsConnected)
|
||||
Disconnected?.Invoke();
|
||||
}
|
||||
|
||||
clientReader?.Dispose();
|
||||
clientWriter?.Dispose();
|
||||
((IDisposable)tcpClient)?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
116
editor/GodotTools/GodotTools.IdeMessaging/Requests/Requests.cs
Normal file
116
editor/GodotTools/GodotTools.IdeMessaging/Requests/Requests.cs
Normal file
@ -0,0 +1,116 @@
|
||||
// ReSharper disable ClassNeverInstantiated.Global
|
||||
// ReSharper disable UnusedMember.Global
|
||||
// ReSharper disable UnusedAutoPropertyAccessor.Global
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace GodotTools.IdeMessaging.Requests
|
||||
{
|
||||
public abstract class Request
|
||||
{
|
||||
[JsonIgnore] public string Id { get; }
|
||||
|
||||
protected Request(string id)
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class Response
|
||||
{
|
||||
[JsonIgnore] public MessageStatus Status { get; set; } = MessageStatus.Ok;
|
||||
}
|
||||
|
||||
public sealed class CodeCompletionRequest : Request
|
||||
{
|
||||
public enum CompletionKind
|
||||
{
|
||||
InputActions = 0,
|
||||
NodePaths,
|
||||
ResourcePaths,
|
||||
ScenePaths,
|
||||
ShaderParams,
|
||||
Signals,
|
||||
ThemeColors,
|
||||
ThemeConstants,
|
||||
ThemeFonts,
|
||||
ThemeStyles
|
||||
}
|
||||
|
||||
public CompletionKind Kind { get; set; }
|
||||
public string ScriptFile { get; set; }
|
||||
|
||||
public new const string Id = "CodeCompletion";
|
||||
|
||||
public CodeCompletionRequest() : base(Id)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CodeCompletionResponse : Response
|
||||
{
|
||||
public CodeCompletionRequest.CompletionKind Kind;
|
||||
public string ScriptFile { get; set; }
|
||||
public string[] Suggestions { get; set; }
|
||||
}
|
||||
|
||||
public sealed class PlayRequest : Request
|
||||
{
|
||||
public new const string Id = "Play";
|
||||
|
||||
public PlayRequest() : base(Id)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PlayResponse : Response
|
||||
{
|
||||
}
|
||||
|
||||
public sealed class DebugPlayRequest : Request
|
||||
{
|
||||
public string DebuggerHost { get; set; }
|
||||
public int DebuggerPort { get; set; }
|
||||
public bool? BuildBeforePlaying { get; set; }
|
||||
|
||||
public new const string Id = "DebugPlay";
|
||||
|
||||
public DebugPlayRequest() : base(Id)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class DebugPlayResponse : Response
|
||||
{
|
||||
}
|
||||
|
||||
public sealed class OpenFileRequest : Request
|
||||
{
|
||||
public string File { get; set; }
|
||||
public int? Line { get; set; }
|
||||
public int? Column { get; set; }
|
||||
|
||||
public new const string Id = "OpenFile";
|
||||
|
||||
public OpenFileRequest() : base(Id)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class OpenFileResponse : Response
|
||||
{
|
||||
}
|
||||
|
||||
public sealed class ReloadScriptsRequest : Request
|
||||
{
|
||||
public new const string Id = "ReloadScripts";
|
||||
|
||||
public ReloadScriptsRequest() : base(Id)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ReloadScriptsResponse : Response
|
||||
{
|
||||
}
|
||||
}
|
23
editor/GodotTools/GodotTools.IdeMessaging/ResponseAwaiter.cs
Normal file
23
editor/GodotTools/GodotTools.IdeMessaging/ResponseAwaiter.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using GodotTools.IdeMessaging.Requests;
|
||||
using GodotTools.IdeMessaging.Utils;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace GodotTools.IdeMessaging
|
||||
{
|
||||
public abstract class ResponseAwaiter : NotifyAwaiter<Response>
|
||||
{
|
||||
public abstract void SetResult(MessageContent content);
|
||||
}
|
||||
|
||||
public class ResponseAwaiter<T> : ResponseAwaiter
|
||||
where T : Response, new()
|
||||
{
|
||||
public override void SetResult(MessageContent content)
|
||||
{
|
||||
if (content.Status == MessageStatus.Ok)
|
||||
SetResult(JsonConvert.DeserializeObject<T>(content.Body));
|
||||
else
|
||||
SetResult(new T {Status = content.Status});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace GodotTools.IdeMessaging.Utils
|
||||
{
|
||||
public class NotifyAwaiter<T> : INotifyCompletion
|
||||
{
|
||||
private Action continuation;
|
||||
private Exception exception;
|
||||
private T result;
|
||||
|
||||
public bool IsCompleted { get; private set; }
|
||||
|
||||
public T GetResult()
|
||||
{
|
||||
if (exception != null)
|
||||
throw exception;
|
||||
return result;
|
||||
}
|
||||
|
||||
public void OnCompleted(Action continuation)
|
||||
{
|
||||
if (this.continuation != null)
|
||||
throw new InvalidOperationException("This awaiter has already been listened");
|
||||
this.continuation = continuation;
|
||||
}
|
||||
|
||||
public void SetResult(T result)
|
||||
{
|
||||
if (IsCompleted)
|
||||
throw new InvalidOperationException("This awaiter is already completed");
|
||||
|
||||
IsCompleted = true;
|
||||
this.result = result;
|
||||
|
||||
continuation?.Invoke();
|
||||
}
|
||||
|
||||
public void SetException(Exception exception)
|
||||
{
|
||||
if (IsCompleted)
|
||||
throw new InvalidOperationException("This awaiter is already completed");
|
||||
|
||||
IsCompleted = true;
|
||||
this.exception = exception;
|
||||
|
||||
continuation?.Invoke();
|
||||
}
|
||||
|
||||
public NotifyAwaiter<T> Reset()
|
||||
{
|
||||
continuation = null;
|
||||
exception = null;
|
||||
result = default(T);
|
||||
IsCompleted = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
public NotifyAwaiter<T> GetAwaiter()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GodotTools.IdeMessaging.Utils
|
||||
{
|
||||
public static class SemaphoreExtensions
|
||||
{
|
||||
public static ConfiguredTaskAwaitable<IDisposable> UseAsync(this SemaphoreSlim semaphoreSlim, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
var wrapper = new SemaphoreSlimWaitReleaseWrapper(semaphoreSlim, out Task waitAsyncTask, cancellationToken);
|
||||
return waitAsyncTask.ContinueWith<IDisposable>(t => wrapper, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private struct SemaphoreSlimWaitReleaseWrapper : IDisposable
|
||||
{
|
||||
private readonly SemaphoreSlim semaphoreSlim;
|
||||
|
||||
public SemaphoreSlimWaitReleaseWrapper(SemaphoreSlim semaphoreSlim, out Task waitAsyncTask, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
this.semaphoreSlim = semaphoreSlim;
|
||||
waitAsyncTask = this.semaphoreSlim.WaitAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
semaphoreSlim.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<ProjectGuid>{EAFFF236-FA96-4A4D-BD23-0E51EF988277}</ProjectGuid>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net472</TargetFramework>
|
||||
<LangVersion>7.2</LangVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
|
||||
<PackageReference Include="EnvDTE" Version="8.0.2" />
|
||||
</ItemGroup>
|
||||
</Project>
|
289
editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs
Normal file
289
editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs
Normal file
@ -0,0 +1,289 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.InteropServices.ComTypes;
|
||||
using System.Text.RegularExpressions;
|
||||
using EnvDTE;
|
||||
|
||||
namespace GodotTools.OpenVisualStudio
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
[DllImport("ole32.dll")]
|
||||
private static extern int GetRunningObjectTable(int reserved, out IRunningObjectTable pprot);
|
||||
|
||||
[DllImport("ole32.dll")]
|
||||
private static extern void CreateBindCtx(int reserved, out IBindCtx ppbc);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||
|
||||
private static void ShowHelp()
|
||||
{
|
||||
Console.WriteLine("Opens the file(s) in a Visual Studio instance that is editing the specified solution.");
|
||||
Console.WriteLine("If an existing instance for the solution is not found, a new one is created.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Usage:");
|
||||
Console.WriteLine(@" GodotTools.OpenVisualStudio.exe solution [file[;line[;col]]...]");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Lines and columns begin at one. Zero or lower will result in an error.");
|
||||
Console.WriteLine("If a line is specified but a column is not, the line is selected in the text editor.");
|
||||
}
|
||||
|
||||
// STAThread needed, otherwise CoRegisterMessageFilter may return CO_E_NOT_SUPPORTED.
|
||||
[STAThread]
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
if (args.Length == 0 || args[0] == "--help" || args[0] == "-h")
|
||||
{
|
||||
ShowHelp();
|
||||
return 0;
|
||||
}
|
||||
|
||||
string solutionFile = NormalizePath(args[0]);
|
||||
|
||||
var dte = FindInstanceEditingSolution(solutionFile);
|
||||
|
||||
if (dte == null)
|
||||
{
|
||||
// Open a new instance
|
||||
dte = TryVisualStudioLaunch("VisualStudio.DTE.17.0");
|
||||
|
||||
if (dte == null)
|
||||
{
|
||||
// Launch of VS 2022 failed, fallback to 2019
|
||||
dte = TryVisualStudioLaunch("VisualStudio.DTE.16.0");
|
||||
}
|
||||
|
||||
dte.UserControl = true;
|
||||
|
||||
try
|
||||
{
|
||||
dte.Solution.Open(solutionFile);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
Console.Error.WriteLine("Solution.Open: Invalid path or file not found");
|
||||
return 1;
|
||||
}
|
||||
|
||||
dte.MainWindow.Visible = true;
|
||||
}
|
||||
|
||||
MessageFilter.Register();
|
||||
|
||||
try
|
||||
{
|
||||
// Open files
|
||||
|
||||
for (int i = 1; i < args.Length; i++)
|
||||
{
|
||||
// Both the line number and the column begin at one
|
||||
|
||||
string[] fileArgumentParts = args[i].Split(';');
|
||||
|
||||
string filePath = NormalizePath(fileArgumentParts[0]);
|
||||
|
||||
try
|
||||
{
|
||||
dte.ItemOperations.OpenFile(filePath);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
Console.Error.WriteLine("ItemOperations.OpenFile: Invalid path or file not found");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (fileArgumentParts.Length > 1)
|
||||
{
|
||||
if (int.TryParse(fileArgumentParts[1], out int line))
|
||||
{
|
||||
var textSelection = (TextSelection)dte.ActiveDocument.Selection;
|
||||
|
||||
if (fileArgumentParts.Length > 2)
|
||||
{
|
||||
if (int.TryParse(fileArgumentParts[2], out int column))
|
||||
{
|
||||
textSelection.MoveToLineAndOffset(line, column);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Error.WriteLine("The column part of the argument must be a valid integer");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
textSelection.GotoLine(line, Select: true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Error.WriteLine("The line part of the argument must be a valid integer");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
var mainWindow = dte.MainWindow;
|
||||
mainWindow.Activate();
|
||||
SetForegroundWindow(new IntPtr(mainWindow.HWnd));
|
||||
|
||||
MessageFilter.Revoke();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static DTE TryVisualStudioLaunch(string version)
|
||||
{
|
||||
try
|
||||
{
|
||||
var visualStudioDteType = Type.GetTypeFromProgID(version, throwOnError: true);
|
||||
var dte = (DTE)Activator.CreateInstance(visualStudioDteType);
|
||||
|
||||
return dte;
|
||||
}
|
||||
catch (COMException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static DTE FindInstanceEditingSolution(string solutionPath)
|
||||
{
|
||||
if (GetRunningObjectTable(0, out IRunningObjectTable pprot) != 0)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
pprot.EnumRunning(out IEnumMoniker ppenumMoniker);
|
||||
ppenumMoniker.Reset();
|
||||
|
||||
var moniker = new IMoniker[1];
|
||||
|
||||
while (ppenumMoniker.Next(1, moniker, IntPtr.Zero) == 0)
|
||||
{
|
||||
string ppszDisplayName;
|
||||
|
||||
CreateBindCtx(0, out IBindCtx ppbc);
|
||||
|
||||
try
|
||||
{
|
||||
moniker[0].GetDisplayName(ppbc, null, out ppszDisplayName);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.ReleaseComObject(ppbc);
|
||||
}
|
||||
|
||||
if (ppszDisplayName == null)
|
||||
continue;
|
||||
|
||||
// The digits after the colon are the process ID
|
||||
if (!Regex.IsMatch(ppszDisplayName, "!VisualStudio.DTE.1[6-7].0:[0-9]"))
|
||||
continue;
|
||||
|
||||
if (pprot.GetObject(moniker[0], out object ppunkObject) == 0)
|
||||
{
|
||||
if (ppunkObject is DTE dte && dte.Solution.FullName.Length > 0)
|
||||
{
|
||||
if (NormalizePath(dte.Solution.FullName) == solutionPath)
|
||||
return dte;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.ReleaseComObject(pprot);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static string NormalizePath(string path)
|
||||
{
|
||||
return new Uri(Path.GetFullPath(path)).LocalPath
|
||||
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
|
||||
.ToUpperInvariant();
|
||||
}
|
||||
|
||||
#region MessageFilter. See: http: //msdn.microsoft.com/en-us/library/ms228772.aspx
|
||||
|
||||
private class MessageFilter : IOleMessageFilter
|
||||
{
|
||||
// Class containing the IOleMessageFilter
|
||||
// thread error-handling functions
|
||||
|
||||
private static IOleMessageFilter _oldFilter;
|
||||
|
||||
// Start the filter
|
||||
public static void Register()
|
||||
{
|
||||
IOleMessageFilter newFilter = new MessageFilter();
|
||||
int ret = CoRegisterMessageFilter(newFilter, out _oldFilter);
|
||||
if (ret != 0)
|
||||
Console.Error.WriteLine($"CoRegisterMessageFilter failed with error code: {ret}");
|
||||
}
|
||||
|
||||
// Done with the filter, close it
|
||||
public static void Revoke()
|
||||
{
|
||||
int ret = CoRegisterMessageFilter(_oldFilter, out _);
|
||||
if (ret != 0)
|
||||
Console.Error.WriteLine($"CoRegisterMessageFilter failed with error code: {ret}");
|
||||
}
|
||||
|
||||
//
|
||||
// IOleMessageFilter functions
|
||||
// Handle incoming thread requests
|
||||
int IOleMessageFilter.HandleInComingCall(int dwCallType, IntPtr hTaskCaller, int dwTickCount, IntPtr lpInterfaceInfo)
|
||||
{
|
||||
// Return the flag SERVERCALL_ISHANDLED
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Thread call was rejected, so try again.
|
||||
int IOleMessageFilter.RetryRejectedCall(IntPtr hTaskCallee, int dwTickCount, int dwRejectType)
|
||||
{
|
||||
if (dwRejectType == 2)
|
||||
// flag = SERVERCALL_RETRYLATER
|
||||
{
|
||||
// Retry the thread call immediately if return >= 0 & < 100
|
||||
return 99;
|
||||
}
|
||||
|
||||
// Too busy; cancel call
|
||||
return -1;
|
||||
}
|
||||
|
||||
int IOleMessageFilter.MessagePending(IntPtr hTaskCallee, int dwTickCount, int dwPendingType)
|
||||
{
|
||||
// Return the flag PENDINGMSG_WAITDEFPROCESS
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Implement the IOleMessageFilter interface
|
||||
[DllImport("ole32.dll")]
|
||||
private static extern int CoRegisterMessageFilter(IOleMessageFilter newFilter, out IOleMessageFilter oldFilter);
|
||||
}
|
||||
|
||||
[ComImport(), Guid("00000016-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
private interface IOleMessageFilter
|
||||
{
|
||||
[PreserveSig]
|
||||
int HandleInComingCall(int dwCallType, IntPtr hTaskCaller, int dwTickCount, IntPtr lpInterfaceInfo);
|
||||
|
||||
[PreserveSig]
|
||||
int RetryRejectedCall(IntPtr hTaskCallee, int dwTickCount, int dwRejectType);
|
||||
|
||||
[PreserveSig]
|
||||
int MessagePending(IntPtr hTaskCallee, int dwTickCount, int dwPendingType);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
namespace GodotTools
|
||||
{
|
||||
public static class ApiAssemblyNames
|
||||
{
|
||||
public const string SolutionName = "GodotSharp";
|
||||
public const string Core = "GodotSharp";
|
||||
public const string Editor = "GodotSharpEditor";
|
||||
}
|
||||
|
||||
public enum ApiAssemblyType
|
||||
{
|
||||
Core,
|
||||
Editor
|
||||
}
|
||||
}
|
165
editor/GodotTools/GodotTools.ProjectEditor/DotNetSolution.cs
Normal file
165
editor/GodotTools/GodotTools.ProjectEditor/DotNetSolution.cs
Normal file
@ -0,0 +1,165 @@
|
||||
using GodotTools.Core;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace GodotTools.ProjectEditor
|
||||
{
|
||||
public class DotNetSolution
|
||||
{
|
||||
private const string _solutionTemplate =
|
||||
@"Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 2012
|
||||
{0}
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
{1}
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{2}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
";
|
||||
|
||||
private const string _projectDeclaration =
|
||||
@"Project(""{{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}}"") = ""{0}"", ""{1}"", ""{{{2}}}""
|
||||
EndProject";
|
||||
|
||||
private const string _solutionPlatformsConfig =
|
||||
@" {0}|Any CPU = {0}|Any CPU";
|
||||
|
||||
private const string _projectPlatformsConfig =
|
||||
@" {{{0}}}.{1}|Any CPU.ActiveCfg = {1}|Any CPU
|
||||
{{{0}}}.{1}|Any CPU.Build.0 = {1}|Any CPU";
|
||||
|
||||
private string _directoryPath;
|
||||
private readonly Dictionary<string, ProjectInfo> _projects = new Dictionary<string, ProjectInfo>();
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public string DirectoryPath
|
||||
{
|
||||
get => _directoryPath;
|
||||
set => _directoryPath = value.IsAbsolutePath() ? value : Path.GetFullPath(value);
|
||||
}
|
||||
|
||||
public class ProjectInfo
|
||||
{
|
||||
public string Guid;
|
||||
public string PathRelativeToSolution;
|
||||
public List<string> Configs = new List<string>();
|
||||
}
|
||||
|
||||
public void AddNewProject(string name, ProjectInfo projectInfo)
|
||||
{
|
||||
_projects[name] = projectInfo;
|
||||
}
|
||||
|
||||
public bool HasProject(string name)
|
||||
{
|
||||
return _projects.ContainsKey(name);
|
||||
}
|
||||
|
||||
public ProjectInfo GetProjectInfo(string name)
|
||||
{
|
||||
return _projects[name];
|
||||
}
|
||||
|
||||
public bool RemoveProject(string name)
|
||||
{
|
||||
return _projects.Remove(name);
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
if (!Directory.Exists(DirectoryPath))
|
||||
throw new FileNotFoundException("The solution directory does not exist.");
|
||||
|
||||
string projectsDecl = string.Empty;
|
||||
string slnPlatformsCfg = string.Empty;
|
||||
string projPlatformsCfg = string.Empty;
|
||||
|
||||
bool isFirstProject = true;
|
||||
|
||||
foreach (var pair in _projects)
|
||||
{
|
||||
string name = pair.Key;
|
||||
ProjectInfo projectInfo = pair.Value;
|
||||
|
||||
if (!isFirstProject)
|
||||
projectsDecl += "\n";
|
||||
|
||||
projectsDecl += string.Format(_projectDeclaration,
|
||||
name, projectInfo.PathRelativeToSolution.Replace("/", "\\"), projectInfo.Guid);
|
||||
|
||||
for (int i = 0; i < projectInfo.Configs.Count; i++)
|
||||
{
|
||||
string config = projectInfo.Configs[i];
|
||||
|
||||
if (i != 0 || !isFirstProject)
|
||||
{
|
||||
slnPlatformsCfg += "\n";
|
||||
projPlatformsCfg += "\n";
|
||||
}
|
||||
|
||||
slnPlatformsCfg += string.Format(_solutionPlatformsConfig, config);
|
||||
projPlatformsCfg += string.Format(_projectPlatformsConfig, projectInfo.Guid, config);
|
||||
}
|
||||
|
||||
isFirstProject = false;
|
||||
}
|
||||
|
||||
string solutionPath = Path.Combine(DirectoryPath, Name + ".sln");
|
||||
string content = string.Format(_solutionTemplate, projectsDecl, slnPlatformsCfg, projPlatformsCfg);
|
||||
|
||||
File.WriteAllText(solutionPath, content, Encoding.UTF8); // UTF-8 with BOM
|
||||
}
|
||||
|
||||
public DotNetSolution(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public static void MigrateFromOldConfigNames(string slnPath)
|
||||
{
|
||||
if (!File.Exists(slnPath))
|
||||
return;
|
||||
|
||||
string input = File.ReadAllText(slnPath);
|
||||
|
||||
if (!Regex.IsMatch(input, Regex.Escape("Tools|Any CPU")))
|
||||
return;
|
||||
|
||||
// This method renames old configurations in solutions to the new ones.
|
||||
//
|
||||
// This is the order configs appear in the solution and what we want to rename them to:
|
||||
// Debug|Any CPU = Debug|Any CPU -> ExportDebug|Any CPU = ExportDebug|Any CPU
|
||||
// Tools|Any CPU = Tools|Any CPU -> Debug|Any CPU = Debug|Any CPU
|
||||
//
|
||||
// But we want to move Tools (now Debug) to the top, so it's easier to rename like this:
|
||||
// Debug|Any CPU = Debug|Any CPU -> Debug|Any CPU = Debug|Any CPU
|
||||
// Release|Any CPU = Release|Any CPU -> ExportDebug|Any CPU = ExportDebug|Any CPU
|
||||
// Tools|Any CPU = Tools|Any CPU -> ExportRelease|Any CPU = ExportRelease|Any CPU
|
||||
|
||||
var dict = new Dictionary<string, string>
|
||||
{
|
||||
{"Debug|Any CPU", "Debug|Any CPU"},
|
||||
{"Release|Any CPU", "ExportDebug|Any CPU"},
|
||||
{"Tools|Any CPU", "ExportRelease|Any CPU"}
|
||||
};
|
||||
|
||||
var regex = new Regex(string.Join("|", dict.Keys.Select(Regex.Escape)));
|
||||
string result = regex.Replace(input, m => dict[m.Value]);
|
||||
|
||||
if (result != input)
|
||||
{
|
||||
// Save a copy of the solution before replacing it
|
||||
FileUtils.SaveBackupCopy(slnPath);
|
||||
|
||||
File.WriteAllText(slnPath, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<ProjectGuid>{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}</ProjectGuid>
|
||||
<TargetFramework>net472</TargetFramework>
|
||||
<LangVersion>7.2</LangVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Build" />
|
||||
<PackageReference Include="Microsoft.Build" Version="16.5.0" />
|
||||
<PackageReference Include="semver" Version="2.0.6" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2019.1.3.0" ExcludeAssets="runtime" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj" />
|
||||
</ItemGroup>
|
||||
<!--
|
||||
The Microsoft.Build.Runtime package is too problematic so we create a MSBuild.exe stub. The workaround described
|
||||
here doesn't work with Microsoft.NETFramework.ReferenceAssemblies: https://github.com/microsoft/msbuild/issues/3486
|
||||
We need a MSBuild.exe file as there's an issue in Microsoft.Build where it executes platform dependent code when
|
||||
searching for MSBuild.exe before the fallback to not using it. A stub is fine as it should never be executed.
|
||||
-->
|
||||
<ItemGroup>
|
||||
<None Include="MSBuild.exe" CopyToOutputDirectory="Always" />
|
||||
</ItemGroup>
|
||||
<Target Name="CopyMSBuildStubWindows" AfterTargets="Build" Condition=" '$(GodotPlatform)' == 'windows' Or ( '$(GodotPlatform)' == '' And '$(OS)' == 'Windows_NT' ) ">
|
||||
<PropertyGroup>
|
||||
<GodotSourceRootPath>$(SolutionDir)/../../../../</GodotSourceRootPath>
|
||||
<GodotOutputDataDir>$(GodotSourceRootPath)/bin/GodotSharp</GodotOutputDataDir>
|
||||
</PropertyGroup>
|
||||
<!-- Need to copy it here as well on Windows -->
|
||||
<Copy SourceFiles="MSBuild.exe" DestinationFiles="$(GodotOutputDataDir)\Mono\lib\mono\v4.0\MSBuild.exe" />
|
||||
</Target>
|
||||
</Project>
|
205
editor/GodotTools/GodotTools.ProjectEditor/IdentifierUtils.cs
Normal file
205
editor/GodotTools/GodotTools.ProjectEditor/IdentifierUtils.cs
Normal file
@ -0,0 +1,205 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace GodotTools.ProjectEditor
|
||||
{
|
||||
public static class IdentifierUtils
|
||||
{
|
||||
public static string SanitizeQualifiedIdentifier(string qualifiedIdentifier, bool allowEmptyIdentifiers)
|
||||
{
|
||||
if (string.IsNullOrEmpty(qualifiedIdentifier))
|
||||
throw new ArgumentException($"{nameof(qualifiedIdentifier)} cannot be empty", nameof(qualifiedIdentifier));
|
||||
|
||||
string[] identifiers = qualifiedIdentifier.Split('.');
|
||||
|
||||
for (int i = 0; i < identifiers.Length; i++)
|
||||
{
|
||||
identifiers[i] = SanitizeIdentifier(identifiers[i], allowEmpty: allowEmptyIdentifiers);
|
||||
}
|
||||
|
||||
return string.Join(".", identifiers);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Skips invalid identifier characters including decimal digit numbers at the start of the identifier.
|
||||
/// </summary>
|
||||
private static void SkipInvalidCharacters(string source, int startIndex, StringBuilder outputBuilder)
|
||||
{
|
||||
for (int i = startIndex; i < source.Length; i++)
|
||||
{
|
||||
char @char = source[i];
|
||||
|
||||
switch (char.GetUnicodeCategory(@char))
|
||||
{
|
||||
case UnicodeCategory.UppercaseLetter:
|
||||
case UnicodeCategory.LowercaseLetter:
|
||||
case UnicodeCategory.TitlecaseLetter:
|
||||
case UnicodeCategory.ModifierLetter:
|
||||
case UnicodeCategory.LetterNumber:
|
||||
case UnicodeCategory.OtherLetter:
|
||||
outputBuilder.Append(@char);
|
||||
break;
|
||||
case UnicodeCategory.NonSpacingMark:
|
||||
case UnicodeCategory.SpacingCombiningMark:
|
||||
case UnicodeCategory.ConnectorPunctuation:
|
||||
case UnicodeCategory.DecimalDigitNumber:
|
||||
// Identifiers may start with underscore
|
||||
if (outputBuilder.Length > startIndex || @char == '_')
|
||||
outputBuilder.Append(@char);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static string SanitizeIdentifier(string identifier, bool allowEmpty)
|
||||
{
|
||||
if (string.IsNullOrEmpty(identifier))
|
||||
{
|
||||
if (allowEmpty)
|
||||
return "Empty"; // Default value for empty identifiers
|
||||
|
||||
throw new ArgumentException($"{nameof(identifier)} cannot be empty if {nameof(allowEmpty)} is false", nameof(identifier));
|
||||
}
|
||||
|
||||
if (identifier.Length > 511)
|
||||
identifier = identifier.Substring(0, 511);
|
||||
|
||||
var identifierBuilder = new StringBuilder();
|
||||
int startIndex = 0;
|
||||
|
||||
if (identifier[0] == '@')
|
||||
{
|
||||
identifierBuilder.Append('@');
|
||||
startIndex += 1;
|
||||
}
|
||||
|
||||
SkipInvalidCharacters(identifier, startIndex, identifierBuilder);
|
||||
|
||||
if (identifierBuilder.Length == startIndex)
|
||||
{
|
||||
// All characters were invalid so now it's empty. Fill it with something.
|
||||
identifierBuilder.Append("Empty");
|
||||
}
|
||||
|
||||
identifier = identifierBuilder.ToString();
|
||||
|
||||
if (identifier[0] != '@' && IsKeyword(identifier, anyDoubleUnderscore: true))
|
||||
identifier = '@' + identifier;
|
||||
|
||||
return identifier;
|
||||
}
|
||||
|
||||
private static bool IsKeyword(string value, bool anyDoubleUnderscore)
|
||||
{
|
||||
// Identifiers that start with double underscore are meant to be used for reserved keywords.
|
||||
// Only existing keywords are enforced, but it may be useful to forbid any identifier
|
||||
// that begins with double underscore to prevent issues with future C# versions.
|
||||
if (anyDoubleUnderscore)
|
||||
{
|
||||
if (value.Length > 2 && value[0] == '_' && value[1] == '_' && value[2] != '_')
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_doubleUnderscoreKeywords.Contains(value))
|
||||
return true;
|
||||
}
|
||||
|
||||
return _keywords.Contains(value);
|
||||
}
|
||||
|
||||
private static readonly HashSet<string> _doubleUnderscoreKeywords = new HashSet<string>
|
||||
{
|
||||
"__arglist",
|
||||
"__makeref",
|
||||
"__reftype",
|
||||
"__refvalue",
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> _keywords = new HashSet<string>
|
||||
{
|
||||
"as",
|
||||
"do",
|
||||
"if",
|
||||
"in",
|
||||
"is",
|
||||
"for",
|
||||
"int",
|
||||
"new",
|
||||
"out",
|
||||
"ref",
|
||||
"try",
|
||||
"base",
|
||||
"bool",
|
||||
"byte",
|
||||
"case",
|
||||
"char",
|
||||
"else",
|
||||
"enum",
|
||||
"goto",
|
||||
"lock",
|
||||
"long",
|
||||
"null",
|
||||
"this",
|
||||
"true",
|
||||
"uint",
|
||||
"void",
|
||||
"break",
|
||||
"catch",
|
||||
"class",
|
||||
"const",
|
||||
"event",
|
||||
"false",
|
||||
"fixed",
|
||||
"float",
|
||||
"sbyte",
|
||||
"short",
|
||||
"throw",
|
||||
"ulong",
|
||||
"using",
|
||||
"where",
|
||||
"while",
|
||||
"yield",
|
||||
"double",
|
||||
"extern",
|
||||
"object",
|
||||
"params",
|
||||
"public",
|
||||
"return",
|
||||
"sealed",
|
||||
"sizeof",
|
||||
"static",
|
||||
"string",
|
||||
"struct",
|
||||
"switch",
|
||||
"typeof",
|
||||
"unsafe",
|
||||
"ushort",
|
||||
"checked",
|
||||
"decimal",
|
||||
"default",
|
||||
"finally",
|
||||
"foreach",
|
||||
"partial",
|
||||
"private",
|
||||
"virtual",
|
||||
"abstract",
|
||||
"continue",
|
||||
"delegate",
|
||||
"explicit",
|
||||
"implicit",
|
||||
"internal",
|
||||
"operator",
|
||||
"override",
|
||||
"readonly",
|
||||
"volatile",
|
||||
"interface",
|
||||
"namespace",
|
||||
"protected",
|
||||
"unchecked",
|
||||
"stackalloc",
|
||||
};
|
||||
}
|
||||
}
|
133
editor/GodotTools/GodotTools.ProjectEditor/ProjectExtensions.cs
Normal file
133
editor/GodotTools/GodotTools.ProjectEditor/ProjectExtensions.cs
Normal file
@ -0,0 +1,133 @@
|
||||
using GodotTools.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Build.Construction;
|
||||
using Microsoft.Build.Globbing;
|
||||
|
||||
namespace GodotTools.ProjectEditor
|
||||
{
|
||||
public static class ProjectExtensions
|
||||
{
|
||||
public static ProjectItemElement FindItemOrNull(this ProjectRootElement root, string itemType, string include, bool noCondition = false)
|
||||
{
|
||||
string normalizedInclude = include.NormalizePath();
|
||||
|
||||
foreach (var itemGroup in root.ItemGroups)
|
||||
{
|
||||
if (noCondition && itemGroup.Condition.Length != 0)
|
||||
continue;
|
||||
|
||||
foreach (var item in itemGroup.Items)
|
||||
{
|
||||
if (item.ItemType != itemType)
|
||||
continue;
|
||||
|
||||
var glob = MSBuildGlob.Parse(item.Include.NormalizePath());
|
||||
|
||||
if (glob.IsMatch(normalizedInclude))
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static ProjectItemElement FindItemOrNullAbs(this ProjectRootElement root, string itemType, string include, bool noCondition = false)
|
||||
{
|
||||
string normalizedInclude = Path.GetFullPath(include).NormalizePath();
|
||||
|
||||
foreach (var itemGroup in root.ItemGroups)
|
||||
{
|
||||
if (noCondition && itemGroup.Condition.Length != 0)
|
||||
continue;
|
||||
|
||||
foreach (var item in itemGroup.Items)
|
||||
{
|
||||
if (item.ItemType != itemType)
|
||||
continue;
|
||||
|
||||
var glob = MSBuildGlob.Parse(Path.GetFullPath(item.Include).NormalizePath());
|
||||
|
||||
if (glob.IsMatch(normalizedInclude))
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static IEnumerable<ProjectItemElement> FindAllItemsInFolder(this ProjectRootElement root, string itemType, string folder)
|
||||
{
|
||||
string absFolderNormalizedWithSep = Path.GetFullPath(folder).NormalizePath() + Path.DirectorySeparatorChar;
|
||||
|
||||
foreach (var itemGroup in root.ItemGroups)
|
||||
{
|
||||
foreach (var item in itemGroup.Items)
|
||||
{
|
||||
if (item.ItemType != itemType)
|
||||
continue;
|
||||
|
||||
string absPathNormalized = Path.GetFullPath(item.Include).NormalizePath();
|
||||
|
||||
if (absPathNormalized.StartsWith(absFolderNormalizedWithSep))
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool HasItem(this ProjectRootElement root, string itemType, string include, bool noCondition = false)
|
||||
{
|
||||
return root.FindItemOrNull(itemType, include, noCondition) != null;
|
||||
}
|
||||
|
||||
public static bool AddItemChecked(this ProjectRootElement root, string itemType, string include)
|
||||
{
|
||||
if (!root.HasItem(itemType, include, noCondition: true))
|
||||
{
|
||||
root.AddItem(itemType, include);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool RemoveItemChecked(this ProjectRootElement root, string itemType, string include)
|
||||
{
|
||||
var item = root.FindItemOrNullAbs(itemType, include);
|
||||
if (item != null)
|
||||
{
|
||||
item.Parent.RemoveChild(item);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Guid GetGuid(this ProjectRootElement root)
|
||||
{
|
||||
foreach (var property in root.Properties)
|
||||
{
|
||||
if (property.Name == "ProjectGuid")
|
||||
return Guid.Parse(property.Value);
|
||||
}
|
||||
|
||||
return Guid.Empty;
|
||||
}
|
||||
|
||||
public static bool AreDefaultCompileItemsEnabled(this ProjectRootElement root)
|
||||
{
|
||||
var enableDefaultCompileItemsProps = root.PropertyGroups
|
||||
.Where(g => string.IsNullOrEmpty(g.Condition))
|
||||
.SelectMany(g => g.Properties
|
||||
.Where(p => p.Name == "EnableDefaultCompileItems" && string.IsNullOrEmpty(p.Condition)));
|
||||
|
||||
bool enableDefaultCompileItems = true;
|
||||
foreach (var prop in enableDefaultCompileItemsProps)
|
||||
enableDefaultCompileItems = prop.Value.Equals("true", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
return enableDefaultCompileItems;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Microsoft.Build.Construction;
|
||||
using Microsoft.Build.Evaluation;
|
||||
|
||||
namespace GodotTools.ProjectEditor
|
||||
{
|
||||
public static class ProjectGenerator
|
||||
{
|
||||
public const string GodotSdkVersionToUse = "3.3.0";
|
||||
public const string GodotSdkNameToUse = "Godot.NET.Sdk";
|
||||
|
||||
public static ProjectRootElement GenGameProject(string name)
|
||||
{
|
||||
if (name.Length == 0)
|
||||
throw new ArgumentException("Project name is empty", nameof(name));
|
||||
|
||||
var root = ProjectRootElement.Create(NewProjectFileOptions.None);
|
||||
|
||||
root.Sdk = $"{GodotSdkNameToUse}/{GodotSdkVersionToUse}";
|
||||
|
||||
var mainGroup = root.AddPropertyGroup();
|
||||
mainGroup.AddProperty("TargetFramework", "net472");
|
||||
|
||||
string sanitizedName = IdentifierUtils.SanitizeQualifiedIdentifier(name, allowEmptyIdentifiers: true);
|
||||
|
||||
// If the name is not a valid namespace, manually set RootNamespace to a sanitized one.
|
||||
if (sanitizedName != name)
|
||||
mainGroup.AddProperty("RootNamespace", sanitizedName);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
public static string GenAndSaveGameProject(string dir, string name)
|
||||
{
|
||||
if (name.Length == 0)
|
||||
throw new ArgumentException("Project name is empty", nameof(name));
|
||||
|
||||
string path = Path.Combine(dir, name + ".csproj");
|
||||
|
||||
var root = GenGameProject(name);
|
||||
|
||||
// Save (without BOM)
|
||||
root.Save(path, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
|
||||
|
||||
return Guid.NewGuid().ToString().ToUpper();
|
||||
}
|
||||
}
|
||||
}
|
471
editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs
Normal file
471
editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs
Normal file
@ -0,0 +1,471 @@
|
||||
using System;
|
||||
using GodotTools.Core;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Build.Construction;
|
||||
using Microsoft.Build.Globbing;
|
||||
using Semver;
|
||||
|
||||
namespace GodotTools.ProjectEditor
|
||||
{
|
||||
public sealed class MSBuildProject
|
||||
{
|
||||
internal ProjectRootElement Root { get; set; }
|
||||
|
||||
public bool HasUnsavedChanges { get; set; }
|
||||
|
||||
public void Save() => Root.Save();
|
||||
|
||||
public MSBuildProject(ProjectRootElement root)
|
||||
{
|
||||
Root = root;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ProjectUtils
|
||||
{
|
||||
public static MSBuildProject Open(string path)
|
||||
{
|
||||
var root = ProjectRootElement.Open(path);
|
||||
return root != null ? new MSBuildProject(root) : null;
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public static void AddItemToProjectChecked(string projectPath, string itemType, string include)
|
||||
{
|
||||
var dir = Directory.GetParent(projectPath).FullName;
|
||||
var root = ProjectRootElement.Open(projectPath);
|
||||
Debug.Assert(root != null);
|
||||
|
||||
if (root.AreDefaultCompileItemsEnabled())
|
||||
{
|
||||
// No need to add. It's already included automatically by the MSBuild Sdk.
|
||||
// This assumes the source file is inside the project directory and not manually excluded in the csproj
|
||||
return;
|
||||
}
|
||||
|
||||
var normalizedInclude = include.RelativeToPath(dir).Replace("/", "\\");
|
||||
|
||||
if (root.AddItemChecked(itemType, normalizedInclude))
|
||||
root.Save();
|
||||
}
|
||||
|
||||
public static void RenameItemInProjectChecked(string projectPath, string itemType, string oldInclude, string newInclude)
|
||||
{
|
||||
var dir = Directory.GetParent(projectPath).FullName;
|
||||
var root = ProjectRootElement.Open(projectPath);
|
||||
Debug.Assert(root != null);
|
||||
|
||||
if (root.AreDefaultCompileItemsEnabled())
|
||||
{
|
||||
// No need to add. It's already included automatically by the MSBuild Sdk.
|
||||
// This assumes the source file is inside the project directory and not manually excluded in the csproj
|
||||
return;
|
||||
}
|
||||
|
||||
var normalizedOldInclude = oldInclude.NormalizePath();
|
||||
var normalizedNewInclude = newInclude.NormalizePath();
|
||||
|
||||
var item = root.FindItemOrNullAbs(itemType, normalizedOldInclude);
|
||||
|
||||
if (item == null)
|
||||
return;
|
||||
|
||||
// Check if the found item include already matches the new path
|
||||
var glob = MSBuildGlob.Parse(item.Include);
|
||||
if (glob.IsMatch(normalizedNewInclude))
|
||||
return;
|
||||
|
||||
// Otherwise, if the item include uses globbing it's better to add a new item instead of modifying
|
||||
if (!string.IsNullOrEmpty(glob.WildcardDirectoryPart) || glob.FilenamePart.Contains("*"))
|
||||
{
|
||||
root.AddItem(itemType, normalizedNewInclude.RelativeToPath(dir).Replace("/", "\\"));
|
||||
root.Save();
|
||||
return;
|
||||
}
|
||||
|
||||
item.Include = normalizedNewInclude.RelativeToPath(dir).Replace("/", "\\");
|
||||
root.Save();
|
||||
}
|
||||
|
||||
public static void RemoveItemFromProjectChecked(string projectPath, string itemType, string include)
|
||||
{
|
||||
var root = ProjectRootElement.Open(projectPath);
|
||||
Debug.Assert(root != null);
|
||||
|
||||
if (root.AreDefaultCompileItemsEnabled())
|
||||
{
|
||||
// No need to add. It's already included automatically by the MSBuild Sdk.
|
||||
// This assumes the source file is inside the project directory and not manually excluded in the csproj
|
||||
return;
|
||||
}
|
||||
|
||||
var normalizedInclude = include.NormalizePath();
|
||||
|
||||
var item = root.FindItemOrNullAbs(itemType, normalizedInclude);
|
||||
|
||||
// Couldn't find an existing item that matches to remove
|
||||
if (item == null)
|
||||
return;
|
||||
|
||||
var glob = MSBuildGlob.Parse(item.Include);
|
||||
|
||||
// If the item include uses globbing don't remove it
|
||||
if (!string.IsNullOrEmpty(glob.WildcardDirectoryPart) || glob.FilenamePart.Contains("*"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
item.Parent.RemoveChild(item);
|
||||
root.Save();
|
||||
}
|
||||
|
||||
public static void RenameItemsToNewFolderInProjectChecked(string projectPath, string itemType, string oldFolder, string newFolder)
|
||||
{
|
||||
var dir = Directory.GetParent(projectPath).FullName;
|
||||
var root = ProjectRootElement.Open(projectPath);
|
||||
Debug.Assert(root != null);
|
||||
|
||||
if (root.AreDefaultCompileItemsEnabled())
|
||||
{
|
||||
// No need to add. It's already included automatically by the MSBuild Sdk.
|
||||
// This assumes the source file is inside the project directory and not manually excluded in the csproj
|
||||
return;
|
||||
}
|
||||
|
||||
bool dirty = false;
|
||||
|
||||
var oldFolderNormalized = oldFolder.NormalizePath();
|
||||
var newFolderNormalized = newFolder.NormalizePath();
|
||||
string absOldFolderNormalized = Path.GetFullPath(oldFolderNormalized).NormalizePath();
|
||||
string absNewFolderNormalized = Path.GetFullPath(newFolderNormalized).NormalizePath();
|
||||
|
||||
foreach (var item in root.FindAllItemsInFolder(itemType, oldFolderNormalized))
|
||||
{
|
||||
string absPathNormalized = Path.GetFullPath(item.Include).NormalizePath();
|
||||
string absNewIncludeNormalized = absNewFolderNormalized + absPathNormalized.Substring(absOldFolderNormalized.Length);
|
||||
item.Include = absNewIncludeNormalized.RelativeToPath(dir).Replace("/", "\\");
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
if (dirty)
|
||||
root.Save();
|
||||
}
|
||||
|
||||
public static void RemoveItemsInFolderFromProjectChecked(string projectPath, string itemType, string folder)
|
||||
{
|
||||
var root = ProjectRootElement.Open(projectPath);
|
||||
Debug.Assert(root != null);
|
||||
|
||||
if (root.AreDefaultCompileItemsEnabled())
|
||||
{
|
||||
// No need to add. It's already included automatically by the MSBuild Sdk.
|
||||
// This assumes the source file is inside the project directory and not manually excluded in the csproj
|
||||
return;
|
||||
}
|
||||
|
||||
var folderNormalized = folder.NormalizePath();
|
||||
|
||||
var itemsToRemove = root.FindAllItemsInFolder(itemType, folderNormalized).ToList();
|
||||
|
||||
if (itemsToRemove.Count > 0)
|
||||
{
|
||||
foreach (var item in itemsToRemove)
|
||||
item.Parent.RemoveChild(item);
|
||||
|
||||
root.Save();
|
||||
}
|
||||
}
|
||||
|
||||
private static string[] GetAllFilesRecursive(string rootDirectory, string mask)
|
||||
{
|
||||
string[] files = Directory.GetFiles(rootDirectory, mask, SearchOption.AllDirectories);
|
||||
|
||||
// We want relative paths
|
||||
for (int i = 0; i < files.Length; i++)
|
||||
{
|
||||
files[i] = files[i].RelativeToPath(rootDirectory);
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
public static string[] GetIncludeFiles(string projectPath, string itemType)
|
||||
{
|
||||
var result = new List<string>();
|
||||
var existingFiles = GetAllFilesRecursive(Path.GetDirectoryName(projectPath), "*.cs");
|
||||
|
||||
var root = ProjectRootElement.Open(projectPath);
|
||||
Debug.Assert(root != null);
|
||||
|
||||
if (root.AreDefaultCompileItemsEnabled())
|
||||
{
|
||||
var excluded = new List<string>();
|
||||
result.AddRange(existingFiles);
|
||||
|
||||
foreach (var item in root.Items)
|
||||
{
|
||||
if (string.IsNullOrEmpty(item.Condition))
|
||||
continue;
|
||||
|
||||
if (item.ItemType != itemType)
|
||||
continue;
|
||||
|
||||
|
||||
string normalizedRemove = item.Remove.NormalizePath();
|
||||
|
||||
var glob = MSBuildGlob.Parse(normalizedRemove);
|
||||
|
||||
excluded.AddRange(result.Where(includedFile => glob.IsMatch(includedFile)));
|
||||
}
|
||||
|
||||
result.RemoveAll(f => excluded.Contains(f));
|
||||
}
|
||||
|
||||
foreach (var itemGroup in root.ItemGroups)
|
||||
{
|
||||
if (itemGroup.Condition.Length != 0)
|
||||
continue;
|
||||
|
||||
foreach (var item in itemGroup.Items)
|
||||
{
|
||||
if (item.ItemType != itemType)
|
||||
continue;
|
||||
|
||||
string normalizedInclude = item.Include.NormalizePath();
|
||||
|
||||
var glob = MSBuildGlob.Parse(normalizedInclude);
|
||||
|
||||
foreach (var existingFile in existingFiles)
|
||||
{
|
||||
if (glob.IsMatch(existingFile))
|
||||
{
|
||||
result.Add(existingFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
public static void MigrateToProjectSdksStyle(MSBuildProject project, string projectName)
|
||||
{
|
||||
var root = project.Root;
|
||||
|
||||
if (!string.IsNullOrEmpty(root.Sdk))
|
||||
return;
|
||||
|
||||
root.Sdk = $"{ProjectGenerator.GodotSdkNameToUse}/{ProjectGenerator.GodotSdkVersionToUse}";
|
||||
|
||||
root.ToolsVersion = null;
|
||||
root.DefaultTargets = null;
|
||||
|
||||
root.AddProperty("TargetFramework", "net472");
|
||||
|
||||
// Remove obsolete properties, items and elements. We're going to be conservative
|
||||
// here to minimize the chances of introducing breaking changes. As such we will
|
||||
// only remove elements that could potentially cause issues with the Godot.NET.Sdk.
|
||||
|
||||
void RemoveElements(IEnumerable<ProjectElement> elements)
|
||||
{
|
||||
foreach (var element in elements)
|
||||
element.Parent.RemoveChild(element);
|
||||
}
|
||||
|
||||
// Default Configuration
|
||||
|
||||
RemoveElements(root.PropertyGroups.SelectMany(g => g.Properties)
|
||||
.Where(p => p.Name == "Configuration" && p.Condition.Trim() == "'$(Configuration)' == ''" && p.Value == "Debug"));
|
||||
|
||||
// Default Platform
|
||||
|
||||
RemoveElements(root.PropertyGroups.SelectMany(g => g.Properties)
|
||||
.Where(p => p.Name == "Platform" && p.Condition.Trim() == "'$(Platform)' == ''" && p.Value == "AnyCPU"));
|
||||
|
||||
// Simple properties
|
||||
|
||||
var yabaiProperties = new[]
|
||||
{
|
||||
"OutputPath",
|
||||
"BaseIntermediateOutputPath",
|
||||
"IntermediateOutputPath",
|
||||
"TargetFrameworkVersion",
|
||||
"ProjectTypeGuids",
|
||||
"ApiConfiguration"
|
||||
};
|
||||
|
||||
RemoveElements(root.PropertyGroups.SelectMany(g => g.Properties)
|
||||
.Where(p => yabaiProperties.Contains(p.Name)));
|
||||
|
||||
// Configuration dependent properties
|
||||
|
||||
var yabaiPropertiesForConfigs = new[]
|
||||
{
|
||||
"DebugSymbols",
|
||||
"DebugType",
|
||||
"Optimize",
|
||||
"DefineConstants",
|
||||
"ErrorReport",
|
||||
"WarningLevel",
|
||||
"ConsolePause"
|
||||
};
|
||||
|
||||
var configNames = new[]
|
||||
{
|
||||
"ExportDebug", "ExportRelease", "Debug",
|
||||
"Tools", "Release" // Include old config names as well in case it's upgrading from 3.2.1 or older
|
||||
};
|
||||
|
||||
foreach (var config in configNames)
|
||||
{
|
||||
var group = root.PropertyGroups
|
||||
.FirstOrDefault(g => g.Condition.Trim() == $"'$(Configuration)|$(Platform)' == '{config}|AnyCPU'");
|
||||
|
||||
if (group == null)
|
||||
continue;
|
||||
|
||||
RemoveElements(group.Properties.Where(p => yabaiPropertiesForConfigs.Contains(p.Name)));
|
||||
|
||||
if (group.Count == 0)
|
||||
{
|
||||
// No more children, safe to delete the group
|
||||
group.Parent.RemoveChild(group);
|
||||
}
|
||||
}
|
||||
|
||||
// Godot API References
|
||||
|
||||
var apiAssemblies = new[] { ApiAssemblyNames.Core, ApiAssemblyNames.Editor };
|
||||
|
||||
RemoveElements(root.ItemGroups.SelectMany(g => g.Items)
|
||||
.Where(i => i.ItemType == "Reference" && apiAssemblies.Contains(i.Include)));
|
||||
|
||||
// Microsoft.NETFramework.ReferenceAssemblies PackageReference
|
||||
|
||||
RemoveElements(root.ItemGroups.SelectMany(g => g.Items).Where(i =>
|
||||
i.ItemType == "PackageReference" &&
|
||||
i.Include.Equals("Microsoft.NETFramework.ReferenceAssemblies", StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
// Imports
|
||||
|
||||
var yabaiImports = new[]
|
||||
{
|
||||
"$(MSBuildBinPath)/Microsoft.CSharp.targets",
|
||||
"$(MSBuildBinPath)Microsoft.CSharp.targets"
|
||||
};
|
||||
|
||||
RemoveElements(root.Imports.Where(import => yabaiImports.Contains(
|
||||
import.Project.Replace("\\", "/").Replace("//", "/"))));
|
||||
|
||||
// 'EnableDefaultCompileItems' and 'GenerateAssemblyInfo' are kept enabled by default
|
||||
// on new projects, but when migrating old projects we disable them to avoid errors.
|
||||
root.AddProperty("EnableDefaultCompileItems", "false");
|
||||
root.AddProperty("GenerateAssemblyInfo", "false");
|
||||
|
||||
// Older AssemblyInfo.cs cause the following error:
|
||||
// 'Properties/AssemblyInfo.cs(19,28): error CS8357:
|
||||
// The specified version string contains wildcards, which are not compatible with determinism.
|
||||
// Either remove wildcards from the version string, or disable determinism for this compilation.'
|
||||
// We disable deterministic builds to prevent this. The user can then fix this manually when desired
|
||||
// by fixing 'AssemblyVersion("1.0.*")' to not use wildcards.
|
||||
root.AddProperty("Deterministic", "false");
|
||||
|
||||
project.HasUnsavedChanges = true;
|
||||
|
||||
var xDoc = XDocument.Parse(root.RawXml);
|
||||
|
||||
if (xDoc.Root == null)
|
||||
return; // Too bad, we will have to keep the xmlns/namespace and xml declaration
|
||||
|
||||
XElement GetElement(XDocument doc, string name, string value, string parentName)
|
||||
{
|
||||
foreach (var node in doc.DescendantNodes())
|
||||
{
|
||||
if (!(node is XElement element))
|
||||
continue;
|
||||
if (element.Name.LocalName.Equals(name) && element.Value == value &&
|
||||
element.Parent != null && element.Parent.Name.LocalName.Equals(parentName))
|
||||
{
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Add comment about Microsoft.NET.Sdk properties disabled during migration
|
||||
|
||||
GetElement(xDoc, name: "EnableDefaultCompileItems", value: "false", parentName: "PropertyGroup")
|
||||
.AddBeforeSelf(new XComment("The following properties were overridden during migration to prevent errors.\n" +
|
||||
" Enabling them may require other manual changes to the project and its files."));
|
||||
|
||||
void RemoveNamespace(XElement element)
|
||||
{
|
||||
element.Attributes().Where(x => x.IsNamespaceDeclaration).Remove();
|
||||
element.Name = element.Name.LocalName;
|
||||
|
||||
foreach (var node in element.DescendantNodes())
|
||||
{
|
||||
if (node is XElement xElement)
|
||||
{
|
||||
// Need to do the same for all children recursively as it adds it to them for some reason...
|
||||
RemoveNamespace(xElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove xmlns/namespace
|
||||
RemoveNamespace(xDoc.Root);
|
||||
|
||||
// Remove xml declaration
|
||||
xDoc.Nodes().FirstOrDefault(node => node.NodeType == XmlNodeType.XmlDeclaration)?.Remove();
|
||||
|
||||
string projectFullPath = root.FullPath;
|
||||
|
||||
root = ProjectRootElement.Create(xDoc.CreateReader());
|
||||
root.FullPath = projectFullPath;
|
||||
|
||||
project.Root = root;
|
||||
}
|
||||
|
||||
public static void EnsureGodotSdkIsUpToDate(MSBuildProject project)
|
||||
{
|
||||
string godotSdkAttrValue = $"{ProjectGenerator.GodotSdkNameToUse}/{ProjectGenerator.GodotSdkVersionToUse}";
|
||||
|
||||
var root = project.Root;
|
||||
string rootSdk = root.Sdk?.Trim();
|
||||
|
||||
if (!string.IsNullOrEmpty(rootSdk))
|
||||
{
|
||||
// Check if the version is already the same.
|
||||
if (rootSdk.Equals(godotSdkAttrValue, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
// We also allow higher versions as long as the major and minor are the same.
|
||||
var semVerToUse = SemVersion.Parse(ProjectGenerator.GodotSdkVersionToUse);
|
||||
var godotSdkAttrLaxValueRegex = new Regex($@"^{ProjectGenerator.GodotSdkNameToUse}/(?<ver>.*)$");
|
||||
|
||||
var match = godotSdkAttrLaxValueRegex.Match(rootSdk);
|
||||
|
||||
if (match.Success &&
|
||||
SemVersion.TryParse(match.Groups["ver"].Value, out var semVerDetected) &&
|
||||
semVerDetected.Major == semVerToUse.Major &&
|
||||
semVerDetected.Minor == semVerToUse.Minor &&
|
||||
semVerDetected > semVerToUse)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
root.Sdk = godotSdkAttrValue;
|
||||
project.HasUnsavedChanges = true;
|
||||
}
|
||||
}
|
||||
}
|
41
editor/GodotTools/GodotTools.sln
Normal file
41
editor/GodotTools/GodotTools.sln
Normal file
@ -0,0 +1,41 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 2012
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.ProjectEditor", "GodotTools.ProjectEditor\GodotTools.ProjectEditor.csproj", "{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools", "GodotTools\GodotTools.csproj", "{27B00618-A6F2-4828-B922-05CAEB08C286}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.Core", "GodotTools.Core\GodotTools.Core.csproj", "{639E48BD-44E5-4091-8EDD-22D36DC0768D}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.BuildLogger", "GodotTools.BuildLogger\GodotTools.BuildLogger.csproj", "{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotTools.OpenVisualStudio", "GodotTools.OpenVisualStudio\GodotTools.OpenVisualStudio.csproj", "{EAFFF236-FA96-4A4D-BD23-0E51EF988277}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{27B00618-A6F2-4828-B922-05CAEB08C286}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{27B00618-A6F2-4828-B922-05CAEB08C286}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{27B00618-A6F2-4828-B922-05CAEB08C286}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{27B00618-A6F2-4828-B922-05CAEB08C286}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{639E48BD-44E5-4091-8EDD-22D36DC0768D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{639E48BD-44E5-4091-8EDD-22D36DC0768D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{639E48BD-44E5-4091-8EDD-22D36DC0768D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{639E48BD-44E5-4091-8EDD-22D36DC0768D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{EAFFF236-FA96-4A4D-BD23-0E51EF988277}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EAFFF236-FA96-4A4D-BD23-0E51EF988277}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EAFFF236-FA96-4A4D-BD23-0E51EF988277}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EAFFF236-FA96-4A4D-BD23-0E51EF988277}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
58
editor/GodotTools/GodotTools/Build/BuildInfo.cs
Normal file
58
editor/GodotTools/GodotTools/Build/BuildInfo.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
using GodotTools.Internals;
|
||||
using Path = System.IO.Path;
|
||||
|
||||
namespace GodotTools.Build
|
||||
{
|
||||
[Serializable]
|
||||
public sealed class BuildInfo : Reference // TODO Remove Reference once we have proper serialization
|
||||
{
|
||||
public string Solution { get; }
|
||||
public string[] Targets { get; }
|
||||
public string Configuration { get; }
|
||||
public bool Restore { get; }
|
||||
// TODO Use List once we have proper serialization
|
||||
public Array<string> CustomProperties { get; } = new Array<string>();
|
||||
|
||||
public string LogsDirPath => Path.Combine(GodotSharpDirs.BuildLogsDirs, $"{Solution.MD5Text()}_{Configuration}");
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj is BuildInfo other)
|
||||
return other.Solution == Solution && other.Targets == Targets &&
|
||||
other.Configuration == Configuration && other.Restore == Restore &&
|
||||
other.CustomProperties == CustomProperties && other.LogsDirPath == LogsDirPath;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int hash = 17;
|
||||
hash = (hash * 29) + Solution.GetHashCode();
|
||||
hash = (hash * 29) + Targets.GetHashCode();
|
||||
hash = (hash * 29) + Configuration.GetHashCode();
|
||||
hash = (hash * 29) + Restore.GetHashCode();
|
||||
hash = (hash * 29) + CustomProperties.GetHashCode();
|
||||
hash = (hash * 29) + LogsDirPath.GetHashCode();
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
|
||||
private BuildInfo()
|
||||
{
|
||||
}
|
||||
|
||||
public BuildInfo(string solution, string[] targets, string configuration, bool restore)
|
||||
{
|
||||
Solution = solution;
|
||||
Targets = targets;
|
||||
Configuration = configuration;
|
||||
Restore = restore;
|
||||
}
|
||||
}
|
||||
}
|
294
editor/GodotTools/GodotTools/Build/BuildManager.cs
Normal file
294
editor/GodotTools/GodotTools/Build/BuildManager.cs
Normal file
@ -0,0 +1,294 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using GodotTools.Ides.Rider;
|
||||
using GodotTools.Internals;
|
||||
using JetBrains.Annotations;
|
||||
using static GodotTools.Internals.Globals;
|
||||
using File = GodotTools.Utils.File;
|
||||
using OS = GodotTools.Utils.OS;
|
||||
using Path = System.IO.Path;
|
||||
|
||||
namespace GodotTools.Build
|
||||
{
|
||||
public static class BuildManager
|
||||
{
|
||||
private static BuildInfo _buildInProgress;
|
||||
|
||||
public const string PropNameMSBuildMono = "MSBuild (Mono)";
|
||||
public const string PropNameMSBuildVs = "MSBuild (VS Build Tools)";
|
||||
public const string PropNameMSBuildJetBrains = "MSBuild (JetBrains Rider)";
|
||||
public const string PropNameDotnetCli = "dotnet CLI";
|
||||
|
||||
public const string MsBuildIssuesFileName = "msbuild_issues.csv";
|
||||
public const string MsBuildLogFileName = "msbuild_log.txt";
|
||||
|
||||
public delegate void BuildLaunchFailedEventHandler(BuildInfo buildInfo, string reason);
|
||||
|
||||
public static event BuildLaunchFailedEventHandler BuildLaunchFailed;
|
||||
public static event Action<BuildInfo> BuildStarted;
|
||||
public static event Action<BuildResult> BuildFinished;
|
||||
public static event Action<string> StdOutputReceived;
|
||||
public static event Action<string> StdErrorReceived;
|
||||
|
||||
private static void RemoveOldIssuesFile(BuildInfo buildInfo)
|
||||
{
|
||||
string issuesFile = GetIssuesFilePath(buildInfo);
|
||||
|
||||
if (!File.Exists(issuesFile))
|
||||
return;
|
||||
|
||||
File.Delete(issuesFile);
|
||||
}
|
||||
|
||||
private static void ShowBuildErrorDialog(string message)
|
||||
{
|
||||
var plugin = GodotSharpEditor.Instance;
|
||||
plugin.ShowErrorDialog(message, "Build error");
|
||||
plugin.MakeBottomPanelItemVisible(plugin.MSBuildPanel);
|
||||
}
|
||||
|
||||
public static void RestartBuild(BuildOutputView buildOutputView) => throw new NotImplementedException();
|
||||
public static void StopBuild(BuildOutputView buildOutputView) => throw new NotImplementedException();
|
||||
|
||||
private static string GetLogFilePath(BuildInfo buildInfo)
|
||||
{
|
||||
return Path.Combine(buildInfo.LogsDirPath, MsBuildLogFileName);
|
||||
}
|
||||
|
||||
private static string GetIssuesFilePath(BuildInfo buildInfo)
|
||||
{
|
||||
return Path.Combine(buildInfo.LogsDirPath, MsBuildIssuesFileName);
|
||||
}
|
||||
|
||||
private static void PrintVerbose(string text)
|
||||
{
|
||||
if (Godot.OS.IsStdoutVerbose())
|
||||
Godot.GD.Print(text);
|
||||
}
|
||||
|
||||
public static bool Build(BuildInfo buildInfo)
|
||||
{
|
||||
if (_buildInProgress != null)
|
||||
throw new InvalidOperationException("A build is already in progress");
|
||||
|
||||
_buildInProgress = buildInfo;
|
||||
|
||||
try
|
||||
{
|
||||
BuildStarted?.Invoke(buildInfo);
|
||||
|
||||
// Required in order to update the build tasks list
|
||||
Internal.GodotMainIteration();
|
||||
|
||||
try
|
||||
{
|
||||
RemoveOldIssuesFile(buildInfo);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
BuildLaunchFailed?.Invoke(buildInfo, $"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}");
|
||||
Console.Error.WriteLine(e);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
int exitCode = BuildSystem.Build(buildInfo, StdOutputReceived, StdErrorReceived);
|
||||
|
||||
if (exitCode != 0)
|
||||
PrintVerbose($"MSBuild exited with code: {exitCode}. Log file: {GetLogFilePath(buildInfo)}");
|
||||
|
||||
BuildFinished?.Invoke(exitCode == 0 ? BuildResult.Success : BuildResult.Error);
|
||||
|
||||
return exitCode == 0;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
BuildLaunchFailed?.Invoke(buildInfo, $"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}");
|
||||
Console.Error.WriteLine(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_buildInProgress = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<bool> BuildAsync(BuildInfo buildInfo)
|
||||
{
|
||||
if (_buildInProgress != null)
|
||||
throw new InvalidOperationException("A build is already in progress");
|
||||
|
||||
_buildInProgress = buildInfo;
|
||||
|
||||
try
|
||||
{
|
||||
BuildStarted?.Invoke(buildInfo);
|
||||
|
||||
try
|
||||
{
|
||||
RemoveOldIssuesFile(buildInfo);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
BuildLaunchFailed?.Invoke(buildInfo, $"Cannot remove issues file: {GetIssuesFilePath(buildInfo)}");
|
||||
Console.Error.WriteLine(e);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
int exitCode = await BuildSystem.BuildAsync(buildInfo, StdOutputReceived, StdErrorReceived);
|
||||
|
||||
if (exitCode != 0)
|
||||
PrintVerbose($"MSBuild exited with code: {exitCode}. Log file: {GetLogFilePath(buildInfo)}");
|
||||
|
||||
BuildFinished?.Invoke(exitCode == 0 ? BuildResult.Success : BuildResult.Error);
|
||||
|
||||
return exitCode == 0;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
BuildLaunchFailed?.Invoke(buildInfo, $"The build method threw an exception.\n{e.GetType().FullName}: {e.Message}");
|
||||
Console.Error.WriteLine(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_buildInProgress = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool BuildProjectBlocking(string config, [CanBeNull] string[] targets = null, [CanBeNull] string platform = null)
|
||||
{
|
||||
var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, targets ?? new[] {"Build"}, config, restore: true);
|
||||
|
||||
// If a platform was not specified, try determining the current one. If that fails, let MSBuild auto-detect it.
|
||||
if (platform != null || OS.PlatformNameMap.TryGetValue(Godot.OS.GetName(), out platform))
|
||||
buildInfo.CustomProperties.Add($"GodotTargetPlatform={platform}");
|
||||
|
||||
if (Internal.GodotIsRealTDouble())
|
||||
buildInfo.CustomProperties.Add("GodotRealTIsDouble=true");
|
||||
|
||||
return BuildProjectBlocking(buildInfo);
|
||||
}
|
||||
|
||||
private static bool BuildProjectBlocking(BuildInfo buildInfo)
|
||||
{
|
||||
if (!File.Exists(buildInfo.Solution))
|
||||
return true; // No solution to build
|
||||
|
||||
// Make sure the API assemblies are up to date before building the project.
|
||||
// We may not have had the chance to update the release API assemblies, and the debug ones
|
||||
// may have been deleted by the user at some point after they were loaded by the Godot editor.
|
||||
string apiAssembliesUpdateError = Internal.UpdateApiAssembliesFromPrebuilt(buildInfo.Configuration == "ExportRelease" ? "Release" : "Debug");
|
||||
|
||||
if (!string.IsNullOrEmpty(apiAssembliesUpdateError))
|
||||
{
|
||||
ShowBuildErrorDialog("Failed to update the Godot API assemblies");
|
||||
return false;
|
||||
}
|
||||
|
||||
using (var pr = new EditorProgress("mono_project_debug_build", "Building project solution...", 1))
|
||||
{
|
||||
pr.Step("Building project solution", 0);
|
||||
|
||||
if (!Build(buildInfo))
|
||||
{
|
||||
ShowBuildErrorDialog("Failed to build project solution");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool EditorBuildCallback()
|
||||
{
|
||||
if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
|
||||
return true; // No solution to build
|
||||
|
||||
GenerateEditorScriptMetadata();
|
||||
|
||||
if (GodotSharpEditor.Instance.SkipBuildBeforePlaying)
|
||||
return true; // Requested play from an external editor/IDE which already built the project
|
||||
|
||||
return BuildProjectBlocking("Debug");
|
||||
}
|
||||
|
||||
// NOTE: This will be replaced with C# source generators in 4.0
|
||||
public static void GenerateEditorScriptMetadata()
|
||||
{
|
||||
string editorScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor");
|
||||
string playerScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor_player");
|
||||
|
||||
CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, editorScriptsMetadataPath);
|
||||
|
||||
if (!File.Exists(editorScriptsMetadataPath))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
File.Copy(editorScriptsMetadataPath, playerScriptsMetadataPath);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new IOException("Failed to copy scripts metadata file.", innerException: e);
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: This will be replaced with C# source generators in 4.0
|
||||
public static string GenerateExportedGameScriptMetadata(bool isDebug)
|
||||
{
|
||||
string scriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, $"scripts_metadata.{(isDebug ? "debug" : "release")}");
|
||||
CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, scriptsMetadataPath);
|
||||
return scriptsMetadataPath;
|
||||
}
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
// Build tool settings
|
||||
var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings();
|
||||
|
||||
BuildTool msbuildDefault;
|
||||
|
||||
if (OS.IsWindows)
|
||||
{
|
||||
if (RiderPathManager.IsExternalEditorSetToRider(editorSettings))
|
||||
msbuildDefault = BuildTool.JetBrainsMsBuild;
|
||||
else
|
||||
msbuildDefault = !string.IsNullOrEmpty(OS.PathWhich("dotnet")) ? BuildTool.DotnetCli : BuildTool.MsBuildVs;
|
||||
}
|
||||
else
|
||||
{
|
||||
msbuildDefault = !string.IsNullOrEmpty(OS.PathWhich("dotnet")) ? BuildTool.DotnetCli : BuildTool.MsBuildMono;
|
||||
}
|
||||
|
||||
EditorDef("mono/builds/build_tool", msbuildDefault);
|
||||
|
||||
string hintString;
|
||||
|
||||
if (OS.IsWindows)
|
||||
{
|
||||
hintString = $"{PropNameMSBuildMono}:{(int)BuildTool.MsBuildMono}," +
|
||||
$"{PropNameMSBuildVs}:{(int)BuildTool.MsBuildVs}," +
|
||||
$"{PropNameMSBuildJetBrains}:{(int)BuildTool.JetBrainsMsBuild}," +
|
||||
$"{PropNameDotnetCli}:{(int)BuildTool.DotnetCli}";
|
||||
}
|
||||
else
|
||||
{
|
||||
hintString = $"{PropNameMSBuildMono}:{(int)BuildTool.MsBuildMono}," +
|
||||
$"{PropNameDotnetCli}:{(int)BuildTool.DotnetCli}";
|
||||
}
|
||||
|
||||
editorSettings.AddPropertyInfo(new Godot.Collections.Dictionary
|
||||
{
|
||||
["type"] = Godot.Variant.Type.Int,
|
||||
["name"] = "mono/builds/build_tool",
|
||||
["hint"] = Godot.PropertyHint.Enum,
|
||||
["hint_string"] = hintString
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
418
editor/GodotTools/GodotTools/Build/BuildOutputView.cs
Normal file
418
editor/GodotTools/GodotTools/Build/BuildOutputView.cs
Normal file
@ -0,0 +1,418 @@
|
||||
using Godot;
|
||||
using System;
|
||||
using Godot.Collections;
|
||||
using GodotTools.Internals;
|
||||
using JetBrains.Annotations;
|
||||
using File = GodotTools.Utils.File;
|
||||
using Path = System.IO.Path;
|
||||
|
||||
namespace GodotTools.Build
|
||||
{
|
||||
public class BuildOutputView : VBoxContainer, ISerializationListener
|
||||
{
|
||||
[Serializable]
|
||||
private class BuildIssue : Reference // TODO Remove Reference once we have proper serialization
|
||||
{
|
||||
public bool Warning { get; set; }
|
||||
public string File { get; set; }
|
||||
public int Line { get; set; }
|
||||
public int Column { get; set; }
|
||||
public string Code { get; set; }
|
||||
public string Message { get; set; }
|
||||
public string ProjectFile { get; set; }
|
||||
}
|
||||
|
||||
[Signal]
|
||||
public delegate void BuildStateChanged();
|
||||
|
||||
public bool HasBuildExited { get; private set; } = false;
|
||||
|
||||
public BuildResult? BuildResult { get; private set; } = null;
|
||||
|
||||
public int ErrorCount { get; private set; } = 0;
|
||||
|
||||
public int WarningCount { get; private set; } = 0;
|
||||
|
||||
public bool ErrorsVisible { get; set; } = true;
|
||||
public bool WarningsVisible { get; set; } = true;
|
||||
|
||||
public Texture BuildStateIcon
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!HasBuildExited)
|
||||
return GetIcon("Stop", "EditorIcons");
|
||||
|
||||
if (BuildResult == Build.BuildResult.Error)
|
||||
return GetIcon("Error", "EditorIcons");
|
||||
|
||||
if (WarningCount > 1)
|
||||
return GetIcon("Warning", "EditorIcons");
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool LogVisible
|
||||
{
|
||||
set => _buildLog.Visible = value;
|
||||
}
|
||||
|
||||
// TODO Use List once we have proper serialization.
|
||||
private readonly Array<BuildIssue> _issues = new Array<BuildIssue>();
|
||||
private ItemList _issuesList;
|
||||
private PopupMenu _issuesListContextMenu;
|
||||
private TextEdit _buildLog;
|
||||
private BuildInfo _buildInfo;
|
||||
|
||||
private readonly object _pendingBuildLogTextLock = new object();
|
||||
[NotNull] private string _pendingBuildLogText = string.Empty;
|
||||
|
||||
private void LoadIssuesFromFile(string csvFile)
|
||||
{
|
||||
using (var file = new Godot.File())
|
||||
{
|
||||
try
|
||||
{
|
||||
Error openError = file.Open(csvFile, Godot.File.ModeFlags.Read);
|
||||
|
||||
if (openError != Error.Ok)
|
||||
return;
|
||||
|
||||
while (!file.EofReached())
|
||||
{
|
||||
string[] csvColumns = file.GetCsvLine();
|
||||
|
||||
if (csvColumns.Length == 1 && string.IsNullOrEmpty(csvColumns[0]))
|
||||
return;
|
||||
|
||||
if (csvColumns.Length != 7)
|
||||
{
|
||||
GD.PushError($"Expected 7 columns, got {csvColumns.Length}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var issue = new BuildIssue
|
||||
{
|
||||
Warning = csvColumns[0] == "warning",
|
||||
File = csvColumns[1],
|
||||
Line = int.Parse(csvColumns[2]),
|
||||
Column = int.Parse(csvColumns[3]),
|
||||
Code = csvColumns[4],
|
||||
Message = csvColumns[5],
|
||||
ProjectFile = csvColumns[6]
|
||||
};
|
||||
|
||||
if (issue.Warning)
|
||||
WarningCount += 1;
|
||||
else
|
||||
ErrorCount += 1;
|
||||
|
||||
_issues.Add(issue);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
file.Close(); // Disposing it is not enough. We need to call Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void IssueActivated(int idx)
|
||||
{
|
||||
if (idx < 0 || idx >= _issuesList.GetItemCount())
|
||||
throw new IndexOutOfRangeException("Item list index out of range");
|
||||
|
||||
// Get correct issue idx from issue list
|
||||
int issueIndex = (int)_issuesList.GetItemMetadata(idx);
|
||||
|
||||
if (issueIndex < 0 || issueIndex >= _issues.Count)
|
||||
throw new IndexOutOfRangeException("Issue index out of range");
|
||||
|
||||
BuildIssue issue = _issues[issueIndex];
|
||||
|
||||
if (string.IsNullOrEmpty(issue.ProjectFile) && string.IsNullOrEmpty(issue.File))
|
||||
return;
|
||||
|
||||
string projectDir = issue.ProjectFile.Length > 0 ? issue.ProjectFile.GetBaseDir() : _buildInfo.Solution.GetBaseDir();
|
||||
|
||||
string file = Path.Combine(projectDir.SimplifyGodotPath(), issue.File.SimplifyGodotPath());
|
||||
|
||||
if (!File.Exists(file))
|
||||
return;
|
||||
|
||||
file = ProjectSettings.LocalizePath(file);
|
||||
|
||||
if (file.StartsWith("res://"))
|
||||
{
|
||||
var script = (Script)ResourceLoader.Load(file, typeHint: Internal.CSharpLanguageType);
|
||||
|
||||
if (script != null && Internal.ScriptEditorEdit(script, issue.Line, issue.Column))
|
||||
Internal.EditorNodeShowScriptScreen();
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateIssuesList()
|
||||
{
|
||||
_issuesList.Clear();
|
||||
|
||||
using (var warningIcon = GetIcon("Warning", "EditorIcons"))
|
||||
using (var errorIcon = GetIcon("Error", "EditorIcons"))
|
||||
{
|
||||
for (int i = 0; i < _issues.Count; i++)
|
||||
{
|
||||
BuildIssue issue = _issues[i];
|
||||
|
||||
if (!(issue.Warning ? WarningsVisible : ErrorsVisible))
|
||||
continue;
|
||||
|
||||
string tooltip = string.Empty;
|
||||
tooltip += $"Message: {issue.Message}";
|
||||
|
||||
if (!string.IsNullOrEmpty(issue.Code))
|
||||
tooltip += $"\nCode: {issue.Code}";
|
||||
|
||||
tooltip += $"\nType: {(issue.Warning ? "warning" : "error")}";
|
||||
|
||||
string text = string.Empty;
|
||||
|
||||
if (!string.IsNullOrEmpty(issue.File))
|
||||
{
|
||||
text += $"{issue.File}({issue.Line},{issue.Column}): ";
|
||||
|
||||
tooltip += $"\nFile: {issue.File}";
|
||||
tooltip += $"\nLine: {issue.Line}";
|
||||
tooltip += $"\nColumn: {issue.Column}";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(issue.ProjectFile))
|
||||
tooltip += $"\nProject: {issue.ProjectFile}";
|
||||
|
||||
text += issue.Message;
|
||||
|
||||
int lineBreakIdx = text.IndexOf("\n", StringComparison.Ordinal);
|
||||
string itemText = lineBreakIdx == -1 ? text : text.Substring(0, lineBreakIdx);
|
||||
_issuesList.AddItem(itemText, issue.Warning ? warningIcon : errorIcon);
|
||||
|
||||
int index = _issuesList.GetItemCount() - 1;
|
||||
_issuesList.SetItemTooltip(index, tooltip);
|
||||
_issuesList.SetItemMetadata(index, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildLaunchFailed(BuildInfo buildInfo, string cause)
|
||||
{
|
||||
HasBuildExited = true;
|
||||
BuildResult = Build.BuildResult.Error;
|
||||
|
||||
_issuesList.Clear();
|
||||
|
||||
var issue = new BuildIssue {Message = cause, Warning = false};
|
||||
|
||||
ErrorCount += 1;
|
||||
_issues.Add(issue);
|
||||
|
||||
UpdateIssuesList();
|
||||
|
||||
EmitSignal(nameof(BuildStateChanged));
|
||||
}
|
||||
|
||||
private void BuildStarted(BuildInfo buildInfo)
|
||||
{
|
||||
_buildInfo = buildInfo;
|
||||
HasBuildExited = false;
|
||||
|
||||
_issues.Clear();
|
||||
WarningCount = 0;
|
||||
ErrorCount = 0;
|
||||
_buildLog.Text = string.Empty;
|
||||
|
||||
UpdateIssuesList();
|
||||
|
||||
EmitSignal(nameof(BuildStateChanged));
|
||||
}
|
||||
|
||||
private void BuildFinished(BuildResult result)
|
||||
{
|
||||
HasBuildExited = true;
|
||||
BuildResult = result;
|
||||
|
||||
LoadIssuesFromFile(Path.Combine(_buildInfo.LogsDirPath, BuildManager.MsBuildIssuesFileName));
|
||||
|
||||
UpdateIssuesList();
|
||||
|
||||
EmitSignal(nameof(BuildStateChanged));
|
||||
}
|
||||
|
||||
private void UpdateBuildLogText()
|
||||
{
|
||||
lock (_pendingBuildLogTextLock)
|
||||
{
|
||||
_buildLog.Text += _pendingBuildLogText;
|
||||
_pendingBuildLogText = string.Empty;
|
||||
ScrollToLastNonEmptyLogLine();
|
||||
}
|
||||
}
|
||||
|
||||
private void StdOutputReceived(string text)
|
||||
{
|
||||
lock (_pendingBuildLogTextLock)
|
||||
{
|
||||
if (_pendingBuildLogText.Length == 0)
|
||||
CallDeferred(nameof(UpdateBuildLogText));
|
||||
_pendingBuildLogText += text + "\n";
|
||||
}
|
||||
}
|
||||
|
||||
private void StdErrorReceived(string text)
|
||||
{
|
||||
lock (_pendingBuildLogTextLock)
|
||||
{
|
||||
if (_pendingBuildLogText.Length == 0)
|
||||
CallDeferred(nameof(UpdateBuildLogText));
|
||||
_pendingBuildLogText += text + "\n";
|
||||
}
|
||||
}
|
||||
|
||||
private void ScrollToLastNonEmptyLogLine()
|
||||
{
|
||||
int line;
|
||||
for (line = _buildLog.GetLineCount(); line > 0; line--)
|
||||
{
|
||||
string lineText = _buildLog.GetLine(line);
|
||||
|
||||
if (!string.IsNullOrEmpty(lineText) || !string.IsNullOrEmpty(lineText?.Trim()))
|
||||
break;
|
||||
}
|
||||
|
||||
_buildLog.CursorSetLine(line);
|
||||
}
|
||||
|
||||
public void RestartBuild()
|
||||
{
|
||||
if (!HasBuildExited)
|
||||
throw new InvalidOperationException("Build already started");
|
||||
|
||||
BuildManager.RestartBuild(this);
|
||||
}
|
||||
|
||||
public void StopBuild()
|
||||
{
|
||||
if (!HasBuildExited)
|
||||
throw new InvalidOperationException("Build is not in progress");
|
||||
|
||||
BuildManager.StopBuild(this);
|
||||
}
|
||||
|
||||
private enum IssuesContextMenuOption
|
||||
{
|
||||
Copy
|
||||
}
|
||||
|
||||
private void IssuesListContextOptionPressed(IssuesContextMenuOption id)
|
||||
{
|
||||
switch (id)
|
||||
{
|
||||
case IssuesContextMenuOption.Copy:
|
||||
{
|
||||
// We don't allow multi-selection but just in case that changes later...
|
||||
string text = null;
|
||||
|
||||
foreach (int issueIndex in _issuesList.GetSelectedItems())
|
||||
{
|
||||
if (text != null)
|
||||
text += "\n";
|
||||
text += _issuesList.GetItemText(issueIndex);
|
||||
}
|
||||
|
||||
if (text != null)
|
||||
OS.Clipboard = text;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(id), id, "Invalid issue context menu option");
|
||||
}
|
||||
}
|
||||
|
||||
private void IssuesListRmbSelected(int index, Vector2 atPosition)
|
||||
{
|
||||
_ = index; // Unused
|
||||
|
||||
_issuesListContextMenu.Clear();
|
||||
_issuesListContextMenu.SetSize(new Vector2(1, 1));
|
||||
|
||||
if (_issuesList.IsAnythingSelected())
|
||||
{
|
||||
// Add menu entries for the selected item
|
||||
_issuesListContextMenu.AddIconItem(GetIcon("ActionCopy", "EditorIcons"),
|
||||
label: "Copy Error".TTR(), (int)IssuesContextMenuOption.Copy);
|
||||
}
|
||||
|
||||
if (_issuesListContextMenu.GetItemCount() > 0)
|
||||
{
|
||||
_issuesListContextMenu.SetPosition(_issuesList.RectGlobalPosition + atPosition);
|
||||
_issuesListContextMenu.Popup_();
|
||||
}
|
||||
}
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
base._Ready();
|
||||
|
||||
SizeFlagsVertical = (int)SizeFlags.ExpandFill;
|
||||
|
||||
var hsc = new HSplitContainer
|
||||
{
|
||||
SizeFlagsHorizontal = (int)SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = (int)SizeFlags.ExpandFill
|
||||
};
|
||||
AddChild(hsc);
|
||||
|
||||
_issuesList = new ItemList
|
||||
{
|
||||
SizeFlagsVertical = (int)SizeFlags.ExpandFill,
|
||||
SizeFlagsHorizontal = (int)SizeFlags.ExpandFill // Avoid being squashed by the build log
|
||||
};
|
||||
_issuesList.Connect("item_activated", this, nameof(IssueActivated));
|
||||
_issuesList.AllowRmbSelect = true;
|
||||
_issuesList.Connect("item_rmb_selected", this, nameof(IssuesListRmbSelected));
|
||||
hsc.AddChild(_issuesList);
|
||||
|
||||
_issuesListContextMenu = new PopupMenu();
|
||||
_issuesListContextMenu.Connect("id_pressed", this, nameof(IssuesListContextOptionPressed));
|
||||
_issuesList.AddChild(_issuesListContextMenu);
|
||||
|
||||
_buildLog = new TextEdit
|
||||
{
|
||||
Readonly = true,
|
||||
SizeFlagsVertical = (int)SizeFlags.ExpandFill,
|
||||
SizeFlagsHorizontal = (int)SizeFlags.ExpandFill // Avoid being squashed by the issues list
|
||||
};
|
||||
hsc.AddChild(_buildLog);
|
||||
|
||||
AddBuildEventListeners();
|
||||
}
|
||||
|
||||
private void AddBuildEventListeners()
|
||||
{
|
||||
BuildManager.BuildLaunchFailed += BuildLaunchFailed;
|
||||
BuildManager.BuildStarted += BuildStarted;
|
||||
BuildManager.BuildFinished += BuildFinished;
|
||||
// StdOutput/Error can be received from different threads, so we need to use CallDeferred
|
||||
BuildManager.StdOutputReceived += StdOutputReceived;
|
||||
BuildManager.StdErrorReceived += StdErrorReceived;
|
||||
}
|
||||
|
||||
public void OnBeforeSerialize()
|
||||
{
|
||||
// In case it didn't update yet. We don't want to have to serialize any pending output.
|
||||
UpdateBuildLogText();
|
||||
}
|
||||
|
||||
public void OnAfterDeserialize()
|
||||
{
|
||||
AddBuildEventListeners(); // Re-add them
|
||||
}
|
||||
}
|
||||
}
|
8
editor/GodotTools/GodotTools/Build/BuildResult.cs
Normal file
8
editor/GodotTools/GodotTools/Build/BuildResult.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace GodotTools.Build
|
||||
{
|
||||
public enum BuildResult
|
||||
{
|
||||
Error,
|
||||
Success
|
||||
}
|
||||
}
|
156
editor/GodotTools/GodotTools/Build/BuildSystem.cs
Normal file
156
editor/GodotTools/GodotTools/Build/BuildSystem.cs
Normal file
@ -0,0 +1,156 @@
|
||||
using GodotTools.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using GodotTools.BuildLogger;
|
||||
using GodotTools.Internals;
|
||||
using GodotTools.Utils;
|
||||
using Directory = System.IO.Directory;
|
||||
|
||||
namespace GodotTools.Build
|
||||
{
|
||||
public static class BuildSystem
|
||||
{
|
||||
private static string MonoWindowsBinDir
|
||||
{
|
||||
get
|
||||
{
|
||||
string monoWinBinDir = Path.Combine(Internal.MonoWindowsInstallRoot, "bin");
|
||||
|
||||
if (!Directory.Exists(monoWinBinDir))
|
||||
throw new FileNotFoundException("Cannot find the Windows Mono install bin directory.");
|
||||
|
||||
return monoWinBinDir;
|
||||
}
|
||||
}
|
||||
|
||||
private static Godot.EditorSettings EditorSettings =>
|
||||
GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings();
|
||||
|
||||
private static bool UsingMonoMsBuildOnWindows
|
||||
{
|
||||
get
|
||||
{
|
||||
if (OS.IsWindows)
|
||||
{
|
||||
return (BuildTool)EditorSettings.GetSetting("mono/builds/build_tool")
|
||||
== BuildTool.MsBuildMono;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static Process LaunchBuild(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler)
|
||||
{
|
||||
(string msbuildPath, BuildTool buildTool) = MsBuildFinder.FindMsBuild();
|
||||
|
||||
if (msbuildPath == null)
|
||||
throw new FileNotFoundException("Cannot find the MSBuild executable.");
|
||||
|
||||
string compilerArgs = BuildArguments(buildTool, buildInfo);
|
||||
|
||||
var startInfo = new ProcessStartInfo(msbuildPath, compilerArgs);
|
||||
|
||||
string launchMessage = $"Running: \"{startInfo.FileName}\" {startInfo.Arguments}";
|
||||
stdOutHandler?.Invoke(launchMessage);
|
||||
if (Godot.OS.IsStdoutVerbose())
|
||||
Console.WriteLine(launchMessage);
|
||||
|
||||
startInfo.RedirectStandardOutput = true;
|
||||
startInfo.RedirectStandardError = true;
|
||||
startInfo.UseShellExecute = false;
|
||||
startInfo.CreateNoWindow = true;
|
||||
|
||||
if (UsingMonoMsBuildOnWindows)
|
||||
{
|
||||
// These environment variables are required for Mono's MSBuild to find the compilers.
|
||||
// We use the batch files in Mono's bin directory to make sure the compilers are executed with mono.
|
||||
string monoWinBinDir = MonoWindowsBinDir;
|
||||
startInfo.EnvironmentVariables.Add("CscToolExe", Path.Combine(monoWinBinDir, "csc.bat"));
|
||||
startInfo.EnvironmentVariables.Add("VbcToolExe", Path.Combine(monoWinBinDir, "vbc.bat"));
|
||||
startInfo.EnvironmentVariables.Add("FscToolExe", Path.Combine(monoWinBinDir, "fsharpc.bat"));
|
||||
}
|
||||
|
||||
// Needed when running from Developer Command Prompt for VS
|
||||
RemovePlatformVariable(startInfo.EnvironmentVariables);
|
||||
|
||||
var process = new Process {StartInfo = startInfo};
|
||||
|
||||
if (stdOutHandler != null)
|
||||
process.OutputDataReceived += (s, e) => stdOutHandler.Invoke(e.Data);
|
||||
if (stdErrHandler != null)
|
||||
process.ErrorDataReceived += (s, e) => stdErrHandler.Invoke(e.Data);
|
||||
|
||||
process.Start();
|
||||
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
return process;
|
||||
}
|
||||
|
||||
public static int Build(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler)
|
||||
{
|
||||
using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler))
|
||||
{
|
||||
process.WaitForExit();
|
||||
|
||||
return process.ExitCode;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<int> BuildAsync(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler)
|
||||
{
|
||||
using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler))
|
||||
{
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
return process.ExitCode;
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildArguments(BuildTool buildTool, BuildInfo buildInfo)
|
||||
{
|
||||
string arguments = string.Empty;
|
||||
|
||||
if (buildTool == BuildTool.DotnetCli)
|
||||
arguments += "msbuild"; // `dotnet msbuild` command
|
||||
|
||||
arguments += $@" ""{buildInfo.Solution}""";
|
||||
|
||||
if (buildInfo.Restore)
|
||||
arguments += " /restore";
|
||||
|
||||
arguments += $@" /t:{string.Join(",", buildInfo.Targets)} " +
|
||||
$@"""/p:{"Configuration=" + buildInfo.Configuration}"" /v:normal " +
|
||||
$@"""/l:{typeof(GodotBuildLogger).FullName},{GodotBuildLogger.AssemblyPath};{buildInfo.LogsDirPath}""";
|
||||
|
||||
foreach (string customProperty in buildInfo.CustomProperties)
|
||||
{
|
||||
arguments += " /p:" + customProperty;
|
||||
}
|
||||
|
||||
return arguments;
|
||||
}
|
||||
|
||||
private static void RemovePlatformVariable(StringDictionary environmentVariables)
|
||||
{
|
||||
// EnvironmentVariables is case sensitive? Seriously?
|
||||
|
||||
var platformEnvironmentVariables = new List<string>();
|
||||
|
||||
foreach (string env in environmentVariables.Keys)
|
||||
{
|
||||
if (env.ToUpper() == "PLATFORM")
|
||||
platformEnvironmentVariables.Add(env);
|
||||
}
|
||||
|
||||
foreach (string env in platformEnvironmentVariables)
|
||||
environmentVariables.Remove(env);
|
||||
}
|
||||
}
|
||||
}
|
10
editor/GodotTools/GodotTools/Build/BuildTool.cs
Normal file
10
editor/GodotTools/GodotTools/Build/BuildTool.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace GodotTools.Build
|
||||
{
|
||||
public enum BuildTool
|
||||
{
|
||||
MsBuildMono,
|
||||
MsBuildVs,
|
||||
JetBrainsMsBuild,
|
||||
DotnetCli
|
||||
}
|
||||
}
|
181
editor/GodotTools/GodotTools/Build/MSBuildPanel.cs
Normal file
181
editor/GodotTools/GodotTools/Build/MSBuildPanel.cs
Normal file
@ -0,0 +1,181 @@
|
||||
using System;
|
||||
using Godot;
|
||||
using GodotTools.Internals;
|
||||
using JetBrains.Annotations;
|
||||
using static GodotTools.Internals.Globals;
|
||||
using File = GodotTools.Utils.File;
|
||||
|
||||
namespace GodotTools.Build
|
||||
{
|
||||
public class MSBuildPanel : VBoxContainer
|
||||
{
|
||||
public BuildOutputView BuildOutputView { get; private set; }
|
||||
|
||||
private MenuButton _buildMenuBtn;
|
||||
private Button _errorsBtn;
|
||||
private Button _warningsBtn;
|
||||
private Button _viewLogBtn;
|
||||
|
||||
private void WarningsToggled(bool pressed)
|
||||
{
|
||||
BuildOutputView.WarningsVisible = pressed;
|
||||
BuildOutputView.UpdateIssuesList();
|
||||
}
|
||||
|
||||
private void ErrorsToggled(bool pressed)
|
||||
{
|
||||
BuildOutputView.ErrorsVisible = pressed;
|
||||
BuildOutputView.UpdateIssuesList();
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
public void BuildSolution()
|
||||
{
|
||||
if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
|
||||
return; // No solution to build
|
||||
|
||||
BuildManager.GenerateEditorScriptMetadata();
|
||||
|
||||
if (!BuildManager.BuildProjectBlocking("Debug"))
|
||||
return; // Build failed
|
||||
|
||||
// Notify running game for hot-reload
|
||||
Internal.ScriptEditorDebuggerReloadScripts();
|
||||
|
||||
// Hot-reload in the editor
|
||||
GodotSharpEditor.Instance.GetNode<HotReloadAssemblyWatcher>("HotReloadAssemblyWatcher").RestartTimer();
|
||||
|
||||
if (Internal.IsAssembliesReloadingNeeded())
|
||||
Internal.ReloadAssemblies(softReload: false);
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
private void RebuildSolution()
|
||||
{
|
||||
if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
|
||||
return; // No solution to build
|
||||
|
||||
BuildManager.GenerateEditorScriptMetadata();
|
||||
|
||||
if (!BuildManager.BuildProjectBlocking("Debug", targets: new[] { "Rebuild" }))
|
||||
return; // Build failed
|
||||
|
||||
// Notify running game for hot-reload
|
||||
Internal.ScriptEditorDebuggerReloadScripts();
|
||||
|
||||
// Hot-reload in the editor
|
||||
GodotSharpEditor.Instance.GetNode<HotReloadAssemblyWatcher>("HotReloadAssemblyWatcher").RestartTimer();
|
||||
|
||||
if (Internal.IsAssembliesReloadingNeeded())
|
||||
Internal.ReloadAssemblies(softReload: false);
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
private void CleanSolution()
|
||||
{
|
||||
if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
|
||||
return; // No solution to build
|
||||
|
||||
BuildManager.BuildProjectBlocking("Debug", targets: new[] { "Clean" });
|
||||
}
|
||||
|
||||
private void ViewLogToggled(bool pressed) => BuildOutputView.LogVisible = pressed;
|
||||
|
||||
private void BuildMenuOptionPressed(BuildMenuOptions id)
|
||||
{
|
||||
switch (id)
|
||||
{
|
||||
case BuildMenuOptions.BuildSolution:
|
||||
BuildSolution();
|
||||
break;
|
||||
case BuildMenuOptions.RebuildSolution:
|
||||
RebuildSolution();
|
||||
break;
|
||||
case BuildMenuOptions.CleanSolution:
|
||||
CleanSolution();
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(id), id, "Invalid build menu option");
|
||||
}
|
||||
}
|
||||
|
||||
private enum BuildMenuOptions
|
||||
{
|
||||
BuildSolution,
|
||||
RebuildSolution,
|
||||
CleanSolution
|
||||
}
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
base._Ready();
|
||||
|
||||
RectMinSize = new Vector2(0, 228) * EditorScale;
|
||||
SizeFlagsVertical = (int)SizeFlags.ExpandFill;
|
||||
|
||||
var toolBarHBox = new HBoxContainer { SizeFlagsHorizontal = (int)SizeFlags.ExpandFill };
|
||||
AddChild(toolBarHBox);
|
||||
|
||||
_buildMenuBtn = new MenuButton { Text = "Build", Icon = GetIcon("Play", "EditorIcons") };
|
||||
toolBarHBox.AddChild(_buildMenuBtn);
|
||||
|
||||
var buildMenu = _buildMenuBtn.GetPopup();
|
||||
buildMenu.AddItem("Build Solution".TTR(), (int)BuildMenuOptions.BuildSolution);
|
||||
buildMenu.AddItem("Rebuild Solution".TTR(), (int)BuildMenuOptions.RebuildSolution);
|
||||
buildMenu.AddItem("Clean Solution".TTR(), (int)BuildMenuOptions.CleanSolution);
|
||||
buildMenu.Connect("id_pressed", this, nameof(BuildMenuOptionPressed));
|
||||
|
||||
_errorsBtn = new Button
|
||||
{
|
||||
HintTooltip = "Show Errors".TTR(),
|
||||
Icon = GetIcon("StatusError", "EditorIcons"),
|
||||
ExpandIcon = false,
|
||||
ToggleMode = true,
|
||||
Pressed = true,
|
||||
FocusMode = FocusModeEnum.None
|
||||
};
|
||||
_errorsBtn.Connect("toggled", this, nameof(ErrorsToggled));
|
||||
toolBarHBox.AddChild(_errorsBtn);
|
||||
|
||||
_warningsBtn = new Button
|
||||
{
|
||||
HintTooltip = "Show Warnings".TTR(),
|
||||
Icon = GetIcon("NodeWarning", "EditorIcons"),
|
||||
ExpandIcon = false,
|
||||
ToggleMode = true,
|
||||
Pressed = true,
|
||||
FocusMode = FocusModeEnum.None
|
||||
};
|
||||
_warningsBtn.Connect("toggled", this, nameof(WarningsToggled));
|
||||
toolBarHBox.AddChild(_warningsBtn);
|
||||
|
||||
_viewLogBtn = new Button
|
||||
{
|
||||
Text = "Show Output".TTR(),
|
||||
ToggleMode = true,
|
||||
Pressed = true,
|
||||
FocusMode = FocusModeEnum.None
|
||||
};
|
||||
_viewLogBtn.Connect("toggled", this, nameof(ViewLogToggled));
|
||||
toolBarHBox.AddChild(_viewLogBtn);
|
||||
|
||||
BuildOutputView = new BuildOutputView();
|
||||
AddChild(BuildOutputView);
|
||||
}
|
||||
|
||||
public override void _Notification(int what)
|
||||
{
|
||||
base._Notification(what);
|
||||
|
||||
if (what == NotificationThemeChanged)
|
||||
{
|
||||
if (_buildMenuBtn != null)
|
||||
_buildMenuBtn.Icon = GetIcon("Play", "EditorIcons");
|
||||
if (_errorsBtn != null)
|
||||
_errorsBtn.Icon = GetIcon("StatusError", "EditorIcons");
|
||||
if (_warningsBtn != null)
|
||||
_warningsBtn.Icon = GetIcon("NodeWarning", "EditorIcons");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
233
editor/GodotTools/GodotTools/Build/MsBuildFinder.cs
Normal file
233
editor/GodotTools/GodotTools/Build/MsBuildFinder.cs
Normal file
@ -0,0 +1,233 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Godot;
|
||||
using GodotTools.Ides.Rider;
|
||||
using GodotTools.Internals;
|
||||
using Directory = System.IO.Directory;
|
||||
using Environment = System.Environment;
|
||||
using File = System.IO.File;
|
||||
using Path = System.IO.Path;
|
||||
using OS = GodotTools.Utils.OS;
|
||||
|
||||
namespace GodotTools.Build
|
||||
{
|
||||
public static class MsBuildFinder
|
||||
{
|
||||
private static string _msbuildToolsPath = string.Empty;
|
||||
private static string _msbuildUnixPath = string.Empty;
|
||||
|
||||
public static (string, BuildTool) FindMsBuild()
|
||||
{
|
||||
var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings();
|
||||
var buildTool = (BuildTool)editorSettings.GetSetting("mono/builds/build_tool");
|
||||
|
||||
if (OS.IsWindows)
|
||||
{
|
||||
switch (buildTool)
|
||||
{
|
||||
case BuildTool.DotnetCli:
|
||||
{
|
||||
string dotnetCliPath = OS.PathWhich("dotnet");
|
||||
if (!string.IsNullOrEmpty(dotnetCliPath))
|
||||
return (dotnetCliPath, BuildTool.DotnetCli);
|
||||
GD.PushError($"Cannot find executable for '{BuildManager.PropNameDotnetCli}'. Fallback to MSBuild from Visual Studio.");
|
||||
goto case BuildTool.MsBuildVs;
|
||||
}
|
||||
case BuildTool.MsBuildVs:
|
||||
{
|
||||
if (string.IsNullOrEmpty(_msbuildToolsPath) || !File.Exists(_msbuildToolsPath))
|
||||
{
|
||||
// Try to search it again if it wasn't found last time or if it was removed from its location
|
||||
_msbuildToolsPath = FindMsBuildToolsPathOnWindows();
|
||||
|
||||
if (string.IsNullOrEmpty(_msbuildToolsPath))
|
||||
throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMSBuildVs}'.");
|
||||
}
|
||||
|
||||
if (!_msbuildToolsPath.EndsWith("\\"))
|
||||
_msbuildToolsPath += "\\";
|
||||
|
||||
return (Path.Combine(_msbuildToolsPath, "MSBuild.exe"), BuildTool.MsBuildVs);
|
||||
}
|
||||
case BuildTool.MsBuildMono:
|
||||
{
|
||||
string msbuildPath = Path.Combine(Internal.MonoWindowsInstallRoot, "bin", "msbuild.bat");
|
||||
|
||||
if (!File.Exists(msbuildPath))
|
||||
throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMSBuildMono}'. Tried with path: {msbuildPath}");
|
||||
|
||||
return (msbuildPath, BuildTool.MsBuildMono);
|
||||
}
|
||||
case BuildTool.JetBrainsMsBuild:
|
||||
{
|
||||
string editorPath = (string)editorSettings.GetSetting(RiderPathManager.EditorPathSettingName);
|
||||
|
||||
if (!File.Exists(editorPath))
|
||||
throw new FileNotFoundException($"Cannot find Rider executable. Tried with path: {editorPath}");
|
||||
|
||||
var riderDir = new FileInfo(editorPath).Directory?.Parent;
|
||||
|
||||
string msbuildPath = Path.Combine(riderDir.FullName, @"tools\MSBuild\Current\Bin\MSBuild.exe");
|
||||
|
||||
if (!File.Exists(msbuildPath))
|
||||
throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMSBuildJetBrains}'. Tried with path: {msbuildPath}");
|
||||
|
||||
return (msbuildPath, BuildTool.JetBrainsMsBuild);
|
||||
}
|
||||
default:
|
||||
throw new IndexOutOfRangeException("Invalid build tool in editor settings");
|
||||
}
|
||||
}
|
||||
|
||||
if (OS.IsUnixLike)
|
||||
{
|
||||
switch (buildTool)
|
||||
{
|
||||
case BuildTool.DotnetCli:
|
||||
{
|
||||
string dotnetCliPath = FindBuildEngineOnUnix("dotnet");
|
||||
if (!string.IsNullOrEmpty(dotnetCliPath))
|
||||
return (dotnetCliPath, BuildTool.DotnetCli);
|
||||
GD.PushError($"Cannot find executable for '{BuildManager.PropNameDotnetCli}'. Fallback to MSBuild from Mono.");
|
||||
goto case BuildTool.MsBuildMono;
|
||||
}
|
||||
case BuildTool.MsBuildMono:
|
||||
{
|
||||
if (string.IsNullOrEmpty(_msbuildUnixPath) || !File.Exists(_msbuildUnixPath))
|
||||
{
|
||||
// Try to search it again if it wasn't found last time or if it was removed from its location
|
||||
_msbuildUnixPath = FindBuildEngineOnUnix("msbuild");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(_msbuildUnixPath))
|
||||
throw new FileNotFoundException($"Cannot find binary for '{BuildManager.PropNameMSBuildMono}'");
|
||||
|
||||
return (_msbuildUnixPath, BuildTool.MsBuildMono);
|
||||
}
|
||||
default:
|
||||
throw new IndexOutOfRangeException("Invalid build tool in editor settings");
|
||||
}
|
||||
}
|
||||
|
||||
throw new PlatformNotSupportedException();
|
||||
}
|
||||
|
||||
private static IEnumerable<string> MsBuildHintDirs
|
||||
{
|
||||
get
|
||||
{
|
||||
var result = new List<string>();
|
||||
|
||||
if (OS.IsOSX)
|
||||
{
|
||||
result.Add("/Library/Frameworks/Mono.framework/Versions/Current/bin/");
|
||||
result.Add("/opt/local/bin/");
|
||||
result.Add("/usr/local/var/homebrew/linked/mono/bin/");
|
||||
result.Add("/usr/local/bin/");
|
||||
result.Add("/usr/local/bin/dotnet/");
|
||||
result.Add("/usr/local/share/dotnet/");
|
||||
}
|
||||
|
||||
result.Add("/opt/novell/mono/bin/");
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private static string FindBuildEngineOnUnix(string name)
|
||||
{
|
||||
string ret = OS.PathWhich(name);
|
||||
|
||||
if (!string.IsNullOrEmpty(ret))
|
||||
return ret;
|
||||
|
||||
string retFallback = OS.PathWhich($"{name}.exe");
|
||||
|
||||
if (!string.IsNullOrEmpty(retFallback))
|
||||
return retFallback;
|
||||
|
||||
foreach (string hintDir in MsBuildHintDirs)
|
||||
{
|
||||
string hintPath = Path.Combine(hintDir, name);
|
||||
|
||||
if (File.Exists(hintPath))
|
||||
return hintPath;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static string FindMsBuildToolsPathOnWindows()
|
||||
{
|
||||
if (!OS.IsWindows)
|
||||
throw new PlatformNotSupportedException();
|
||||
|
||||
// Try to find 15.0 with vswhere
|
||||
|
||||
string[] envNames = Internal.GodotIs32Bits() ?
|
||||
envNames = new[] { "ProgramFiles", "ProgramW6432" } :
|
||||
envNames = new[] { "ProgramFiles(x86)", "ProgramFiles" };
|
||||
|
||||
string vsWherePath = null;
|
||||
foreach (var envName in envNames)
|
||||
{
|
||||
vsWherePath = Environment.GetEnvironmentVariable(envName);
|
||||
if (!string.IsNullOrEmpty(vsWherePath))
|
||||
{
|
||||
vsWherePath += "\\Microsoft Visual Studio\\Installer\\vswhere.exe";
|
||||
if (File.Exists(vsWherePath))
|
||||
break;
|
||||
}
|
||||
|
||||
vsWherePath = null;
|
||||
}
|
||||
|
||||
var vsWhereArgs = new[] {"-latest", "-products", "*", "-requires", "Microsoft.Component.MSBuild"};
|
||||
|
||||
var outputArray = new Godot.Collections.Array<string>();
|
||||
int exitCode = Godot.OS.Execute(vsWherePath, vsWhereArgs,
|
||||
blocking: true, output: (Godot.Collections.Array)outputArray);
|
||||
|
||||
if (exitCode != 0)
|
||||
return string.Empty;
|
||||
|
||||
if (outputArray.Count == 0)
|
||||
return string.Empty;
|
||||
|
||||
var lines = outputArray[0].Split('\n');
|
||||
|
||||
foreach (string line in lines)
|
||||
{
|
||||
int sepIdx = line.IndexOf(':');
|
||||
|
||||
if (sepIdx <= 0)
|
||||
continue;
|
||||
|
||||
string key = line.Substring(0, sepIdx); // No need to trim
|
||||
|
||||
if (key != "installationPath")
|
||||
continue;
|
||||
|
||||
string value = line.Substring(sepIdx + 1).StripEdges();
|
||||
|
||||
if (string.IsNullOrEmpty(value))
|
||||
throw new FormatException("installationPath value is empty");
|
||||
|
||||
if (!value.EndsWith("\\"))
|
||||
value += "\\";
|
||||
|
||||
// Since VS2019, the directory is simply named "Current"
|
||||
string msbuildDir = Path.Combine(value, "MSBuild\\Current\\Bin");
|
||||
|
||||
if (Directory.Exists(msbuildDir))
|
||||
return msbuildDir;
|
||||
|
||||
// Directory name "15.0" is used in VS 2017
|
||||
return Path.Combine(value, "MSBuild\\15.0\\Bin");
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
108
editor/GodotTools/GodotTools/CsProjOperations.cs
Normal file
108
editor/GodotTools/GodotTools/CsProjOperations.cs
Normal file
@ -0,0 +1,108 @@
|
||||
using Godot;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Godot.Collections;
|
||||
using GodotTools.Internals;
|
||||
using GodotTools.ProjectEditor;
|
||||
using File = GodotTools.Utils.File;
|
||||
using Directory = GodotTools.Utils.Directory;
|
||||
|
||||
namespace GodotTools
|
||||
{
|
||||
public static class CsProjOperations
|
||||
{
|
||||
public static string GenerateGameProject(string dir, string name)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ProjectGenerator.GenAndSaveGameProject(dir, name);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
GD.PushError(e.ToString());
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
private static ulong ConvertToTimestamp(this DateTime value)
|
||||
{
|
||||
TimeSpan elapsedTime = value - Epoch;
|
||||
return (ulong)elapsedTime.TotalSeconds;
|
||||
}
|
||||
|
||||
private static bool TryParseFileMetadata(string includeFile, ulong modifiedTime, out Dictionary fileMetadata)
|
||||
{
|
||||
fileMetadata = null;
|
||||
|
||||
var parseError = ScriptClassParser.ParseFile(includeFile, out var classes, out string errorStr);
|
||||
|
||||
if (parseError != Error.Ok)
|
||||
{
|
||||
GD.PushError($"Failed to determine namespace and class for script: {includeFile}. Parse error: {errorStr ?? parseError.ToString()}");
|
||||
return false;
|
||||
}
|
||||
|
||||
string searchName = System.IO.Path.GetFileNameWithoutExtension(includeFile);
|
||||
|
||||
var firstMatch = classes.FirstOrDefault(classDecl =>
|
||||
classDecl.BaseCount != 0 && // If it doesn't inherit anything, it can't be a Godot.Object.
|
||||
classDecl.SearchName == searchName // Filter by the name we're looking for
|
||||
);
|
||||
|
||||
if (firstMatch == null)
|
||||
return false; // Not found
|
||||
|
||||
fileMetadata = new Dictionary
|
||||
{
|
||||
["modified_time"] = $"{modifiedTime}",
|
||||
["class"] = new Dictionary
|
||||
{
|
||||
["namespace"] = firstMatch.Namespace,
|
||||
["class_name"] = firstMatch.Name,
|
||||
["nested"] = firstMatch.Nested
|
||||
}
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void GenerateScriptsMetadata(string projectPath, string outputPath)
|
||||
{
|
||||
var metadataDict = Internal.GetScriptsMetadataOrNothing().Duplicate();
|
||||
|
||||
bool IsUpToDate(string includeFile, ulong modifiedTime)
|
||||
{
|
||||
return metadataDict.TryGetValue(includeFile, out var oldFileVar) &&
|
||||
ulong.TryParse(((Dictionary)oldFileVar)["modified_time"] as string,
|
||||
out ulong storedModifiedTime) && storedModifiedTime == modifiedTime;
|
||||
}
|
||||
|
||||
var outdatedFiles = ProjectUtils.GetIncludeFiles(projectPath, "Compile")
|
||||
.Select(path => ("res://" + path).SimplifyGodotPath())
|
||||
.ToDictionary(path => path, path => File.GetLastWriteTime(path).ConvertToTimestamp())
|
||||
.Where(pair => !IsUpToDate(includeFile: pair.Key, modifiedTime: pair.Value))
|
||||
.ToArray();
|
||||
|
||||
foreach (var pair in outdatedFiles)
|
||||
{
|
||||
metadataDict.Remove(pair.Key);
|
||||
|
||||
string includeFile = pair.Key;
|
||||
|
||||
if (TryParseFileMetadata(includeFile, modifiedTime: pair.Value, out var fileMetadata))
|
||||
metadataDict[includeFile] = fileMetadata;
|
||||
}
|
||||
|
||||
string json = metadataDict.Count <= 0 ? "{}" : JSON.Print(metadataDict);
|
||||
|
||||
string baseDir = outputPath.GetBaseDir();
|
||||
|
||||
if (!Directory.Exists(baseDir))
|
||||
Directory.CreateDirectory(baseDir);
|
||||
|
||||
File.WriteAllText(outputPath, json);
|
||||
}
|
||||
}
|
||||
}
|
834
editor/GodotTools/GodotTools/Export/AotBuilder.cs
Normal file
834
editor/GodotTools/GodotTools/Export/AotBuilder.cs
Normal file
@ -0,0 +1,834 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Godot;
|
||||
using GodotTools.Core;
|
||||
using GodotTools.Internals;
|
||||
using Mono.Cecil;
|
||||
using Directory = GodotTools.Utils.Directory;
|
||||
using File = GodotTools.Utils.File;
|
||||
using OS = GodotTools.Utils.OS;
|
||||
using Path = System.IO.Path;
|
||||
|
||||
namespace GodotTools.Export
|
||||
{
|
||||
public struct AotOptions
|
||||
{
|
||||
public bool EnableLLVM;
|
||||
public bool LLVMOnly;
|
||||
public string LLVMPath;
|
||||
public string LLVMOutputPath;
|
||||
|
||||
public bool FullAot;
|
||||
|
||||
private bool _useInterpreter;
|
||||
|
||||
public bool UseInterpreter
|
||||
{
|
||||
get => _useInterpreter && !LLVMOnly;
|
||||
set => _useInterpreter = value;
|
||||
}
|
||||
|
||||
public string[] ExtraAotOptions;
|
||||
public string[] ExtraOptimizerOptions;
|
||||
|
||||
public string ToolchainPath;
|
||||
}
|
||||
|
||||
public static class AotBuilder
|
||||
{
|
||||
public static void CompileAssemblies(ExportPlugin exporter, AotOptions aotOpts, string[] features, string platform, bool isDebug, string bclDir, string outputDir, string outputDataDir, IDictionary<string, string> assemblies)
|
||||
{
|
||||
// TODO: WASM
|
||||
|
||||
string aotTempDir = Path.Combine(Path.GetTempPath(), $"godot-aot-{Process.GetCurrentProcess().Id}");
|
||||
|
||||
if (!Directory.Exists(aotTempDir))
|
||||
Directory.CreateDirectory(aotTempDir);
|
||||
|
||||
var assembliesPrepared = new Dictionary<string, string>();
|
||||
|
||||
foreach (var dependency in assemblies)
|
||||
{
|
||||
string assemblyName = dependency.Key;
|
||||
string assemblyPath = dependency.Value;
|
||||
|
||||
string assemblyPathInBcl = Path.Combine(bclDir, assemblyName + ".dll");
|
||||
|
||||
if (File.Exists(assemblyPathInBcl))
|
||||
{
|
||||
// Don't create teporaries for assemblies from the BCL
|
||||
assembliesPrepared.Add(assemblyName, assemblyPathInBcl);
|
||||
}
|
||||
else
|
||||
{
|
||||
string tempAssemblyPath = Path.Combine(aotTempDir, assemblyName + ".dll");
|
||||
File.Copy(assemblyPath, tempAssemblyPath);
|
||||
assembliesPrepared.Add(assemblyName, tempAssemblyPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (platform == OS.Platforms.iOS)
|
||||
{
|
||||
string[] architectures = GetEnablediOSArchs(features).ToArray();
|
||||
CompileAssembliesForiOS(exporter, isDebug, architectures, aotOpts, aotTempDir, assembliesPrepared, bclDir);
|
||||
}
|
||||
else if (platform == OS.Platforms.Android)
|
||||
{
|
||||
string[] abis = GetEnabledAndroidAbis(features).ToArray();
|
||||
CompileAssembliesForAndroid(exporter, isDebug, abis, aotOpts, aotTempDir, assembliesPrepared, bclDir);
|
||||
}
|
||||
else
|
||||
{
|
||||
string bits = features.Contains("64") ? "64" : features.Contains("32") ? "32" : null;
|
||||
CompileAssembliesForDesktop(exporter, platform, isDebug, bits, aotOpts, aotTempDir, outputDataDir, assembliesPrepared, bclDir);
|
||||
}
|
||||
}
|
||||
|
||||
public static void CompileAssembliesForAndroid(ExportPlugin exporter, bool isDebug, string[] abis, AotOptions aotOpts, string aotTempDir, IDictionary<string, string> assemblies, string bclDir)
|
||||
{
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
string assemblyName = assembly.Key;
|
||||
string assemblyPath = assembly.Value;
|
||||
|
||||
// Not sure if the 'lib' prefix is an Android thing or just Godot being picky,
|
||||
// but we use '-aot-' as well just in case to avoid conflicts with other libs.
|
||||
string outputFileName = "lib-aot-" + assemblyName + ".dll.so";
|
||||
|
||||
foreach (string abi in abis)
|
||||
{
|
||||
string aotAbiTempDir = Path.Combine(aotTempDir, abi);
|
||||
string soFilePath = Path.Combine(aotAbiTempDir, outputFileName);
|
||||
|
||||
var compilerArgs = GetAotCompilerArgs(OS.Platforms.Android, isDebug, abi, aotOpts, assemblyPath, soFilePath);
|
||||
|
||||
// Make sure the output directory exists
|
||||
Directory.CreateDirectory(aotAbiTempDir);
|
||||
|
||||
string compilerDirPath = Path.Combine(GodotSharpDirs.DataEditorToolsDir, "aot-compilers", $"{OS.Platforms.Android}-{abi}");
|
||||
|
||||
ExecuteCompiler(FindCrossCompiler(compilerDirPath), compilerArgs, bclDir);
|
||||
|
||||
// The Godot exporter expects us to pass the abi in the tags parameter
|
||||
exporter.AddSharedObject(soFilePath, tags: new[] { abi });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void CompileAssembliesForDesktop(ExportPlugin exporter, string platform, bool isDebug, string bits, AotOptions aotOpts, string aotTempDir, string outputDataDir, IDictionary<string, string> assemblies, string bclDir)
|
||||
{
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
string assemblyName = assembly.Key;
|
||||
string assemblyPath = assembly.Value;
|
||||
|
||||
string outputFileExtension = platform == OS.Platforms.Windows ? ".dll" :
|
||||
platform == OS.Platforms.OSX ? ".dylib" :
|
||||
".so";
|
||||
|
||||
string outputFileName = assemblyName + ".dll" + outputFileExtension;
|
||||
string tempOutputFilePath = Path.Combine(aotTempDir, outputFileName);
|
||||
|
||||
var compilerArgs = GetAotCompilerArgs(platform, isDebug, bits, aotOpts, assemblyPath, tempOutputFilePath);
|
||||
|
||||
string compilerDirPath = GetMonoCrossDesktopDirName(platform, bits);
|
||||
|
||||
ExecuteCompiler(FindCrossCompiler(compilerDirPath), compilerArgs, bclDir);
|
||||
|
||||
if (platform == OS.Platforms.OSX)
|
||||
{
|
||||
exporter.AddSharedObject(tempOutputFilePath, tags: null);
|
||||
}
|
||||
else
|
||||
{
|
||||
string libDir = platform == OS.Platforms.Windows ? "bin" : "lib";
|
||||
string outputDataLibDir = Path.Combine(outputDataDir, "Mono", libDir);
|
||||
File.Copy(tempOutputFilePath, Path.Combine(outputDataLibDir, outputFileName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void CompileAssembliesForiOS(ExportPlugin exporter, bool isDebug, string[] architectures, AotOptions aotOpts, string aotTempDir, IDictionary<string, string> assemblies, string bclDir)
|
||||
{
|
||||
void RunAr(IEnumerable<string> objFilePaths, string outputFilePath)
|
||||
{
|
||||
var arArgs = new List<string>()
|
||||
{
|
||||
"cr",
|
||||
outputFilePath
|
||||
};
|
||||
|
||||
foreach (string objFilePath in objFilePaths)
|
||||
arArgs.Add(objFilePath);
|
||||
|
||||
int arExitCode = OS.ExecuteCommand(XcodeHelper.FindXcodeTool("ar"), arArgs);
|
||||
if (arExitCode != 0)
|
||||
throw new Exception($"Command 'ar' exited with code: {arExitCode}");
|
||||
}
|
||||
|
||||
void RunLipo(IEnumerable<string> libFilePaths, string outputFilePath)
|
||||
{
|
||||
var lipoArgs = new List<string>();
|
||||
lipoArgs.Add("-create");
|
||||
lipoArgs.AddRange(libFilePaths);
|
||||
lipoArgs.Add("-output");
|
||||
lipoArgs.Add(outputFilePath);
|
||||
|
||||
int lipoExitCode = OS.ExecuteCommand(XcodeHelper.FindXcodeTool("lipo"), lipoArgs);
|
||||
if (lipoExitCode != 0)
|
||||
throw new Exception($"Command 'lipo' exited with code: {lipoExitCode}");
|
||||
}
|
||||
|
||||
void CreateDummyLibForSimulator(string name, string xcFrameworkPath = null)
|
||||
{
|
||||
xcFrameworkPath = xcFrameworkPath ?? MonoFrameworkFromTemplate(name);
|
||||
string simulatorSubDir = Path.Combine(xcFrameworkPath, "ios-arm64_x86_64-simulator");
|
||||
|
||||
string libFilePath = Path.Combine(simulatorSubDir, name + ".a");
|
||||
|
||||
if (File.Exists(libFilePath))
|
||||
return;
|
||||
|
||||
string CompileForArch(string arch)
|
||||
{
|
||||
string baseFilePath = Path.Combine(aotTempDir, $"{name}.{arch}");
|
||||
string sourceFilePath = baseFilePath + ".c";
|
||||
|
||||
string source = $"int _{AssemblyNameToAotSymbol(name)}() {{ return 0; }}\n";
|
||||
File.WriteAllText(sourceFilePath, source);
|
||||
|
||||
const string iOSPlatformName = "iPhoneSimulator";
|
||||
const string versionMin = "10.0";
|
||||
string iOSSdkPath = Path.Combine(XcodeHelper.XcodePath,
|
||||
$"Contents/Developer/Platforms/{iOSPlatformName}.platform/Developer/SDKs/{iOSPlatformName}.sdk");
|
||||
|
||||
string objFilePath = baseFilePath + ".o";
|
||||
|
||||
var clangArgs = new[]
|
||||
{
|
||||
"-isysroot", iOSSdkPath,
|
||||
$"-miphonesimulator-version-min={versionMin}",
|
||||
"-arch", arch,
|
||||
"-c",
|
||||
"-o", objFilePath,
|
||||
sourceFilePath
|
||||
};
|
||||
|
||||
int clangExitCode = OS.ExecuteCommand(XcodeHelper.FindXcodeTool("clang"), clangArgs);
|
||||
if (clangExitCode != 0)
|
||||
throw new Exception($"Command 'clang' exited with code: {clangExitCode}");
|
||||
|
||||
string arOutputFilePath = Path.Combine(aotTempDir, baseFilePath + ".a");
|
||||
RunAr(new[] {objFilePath}, arOutputFilePath);
|
||||
|
||||
return arOutputFilePath;
|
||||
}
|
||||
|
||||
RunLipo(new[] {CompileForArch("arm64"), CompileForArch("x86_64")}, libFilePath);
|
||||
}
|
||||
|
||||
string projectAssemblyName = GodotSharpDirs.ProjectAssemblyName;
|
||||
string libAotName = $"lib-aot-{projectAssemblyName}";
|
||||
|
||||
string libAotXcFrameworkPath = Path.Combine(aotTempDir, $"{libAotName}.xcframework");
|
||||
string libAotXcFrameworkDevicePath = Path.Combine(libAotXcFrameworkPath, "ios-arm64");
|
||||
string libAotXcFrameworkSimPath = Path.Combine(libAotXcFrameworkPath, "ios-arm64_x86_64-simulator");
|
||||
|
||||
Directory.CreateDirectory(libAotXcFrameworkPath);
|
||||
Directory.CreateDirectory(libAotXcFrameworkDevicePath);
|
||||
Directory.CreateDirectory(libAotXcFrameworkSimPath);
|
||||
|
||||
string libAotFileName = $"{libAotName}.a";
|
||||
string libAotFilePath = Path.Combine(libAotXcFrameworkDevicePath, libAotFileName);
|
||||
|
||||
var cppCode = new StringBuilder();
|
||||
var aotModuleInfoSymbols = new List<string>(assemblies.Count);
|
||||
|
||||
var aotObjFilePaths = new List<string>(assemblies.Count);
|
||||
|
||||
string compilerDirPath = Path.Combine(GodotSharpDirs.DataEditorToolsDir, "aot-compilers",
|
||||
$"{OS.Platforms.iOS}-arm64");
|
||||
string crossCompiler = FindCrossCompiler(compilerDirPath);
|
||||
|
||||
string aotCacheDir = Path.Combine(ProjectSettings.GlobalizePath(GodotSharpDirs.ResTempDir),
|
||||
"obj", isDebug ? "ExportDebug" : "ExportRelease", "godot-aot-cache");
|
||||
|
||||
if (!Directory.Exists(aotCacheDir))
|
||||
Directory.CreateDirectory(aotCacheDir);
|
||||
|
||||
var aotCache = new AotCache(Path.Combine(aotCacheDir, "cache.json"));
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
string assemblyName = assembly.Key;
|
||||
string assemblyPath = assembly.Value;
|
||||
|
||||
string asmFilePath = Path.Combine(aotCacheDir, assemblyName + ".dll.S");
|
||||
string objFilePath = Path.Combine(aotCacheDir, assemblyName + ".dll.o");
|
||||
|
||||
aotCache.RunCached(name: assemblyName, input: assemblyPath, output: objFilePath, () =>
|
||||
{
|
||||
Console.WriteLine($"AOT compiler: Compiling '{assemblyName}'...");
|
||||
|
||||
var compilerArgs = GetAotCompilerArgs(OS.Platforms.iOS, isDebug,
|
||||
"arm64", aotOpts, assemblyPath, asmFilePath);
|
||||
|
||||
ExecuteCompiler(crossCompiler, compilerArgs, bclDir);
|
||||
|
||||
// Assembling
|
||||
const string iOSPlatformName = "iPhoneOS";
|
||||
const string versionMin = "10.0"; // TODO: Turn this hard-coded version into an exporter setting
|
||||
string iOSSdkPath = Path.Combine(XcodeHelper.XcodePath,
|
||||
$"Contents/Developer/Platforms/{iOSPlatformName}.platform/Developer/SDKs/{iOSPlatformName}.sdk");
|
||||
|
||||
var clangArgs = new List<string>()
|
||||
{
|
||||
"-isysroot", iOSSdkPath,
|
||||
"-Qunused-arguments",
|
||||
$"-miphoneos-version-min={versionMin}",
|
||||
"-arch", "arm64",
|
||||
"-c",
|
||||
"-o", objFilePath,
|
||||
"-x", "assembler"
|
||||
};
|
||||
|
||||
if (isDebug)
|
||||
clangArgs.Add("-DDEBUG");
|
||||
|
||||
clangArgs.Add(asmFilePath);
|
||||
|
||||
int clangExitCode = OS.ExecuteCommand(XcodeHelper.FindXcodeTool("clang"), clangArgs);
|
||||
if (clangExitCode != 0)
|
||||
throw new Exception($"Command 'clang' exited with code: {clangExitCode}");
|
||||
});
|
||||
|
||||
aotObjFilePaths.Add(objFilePath);
|
||||
|
||||
aotModuleInfoSymbols.Add($"mono_aot_module_{AssemblyNameToAotSymbol(assemblyName)}_info");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
aotCache.SaveCache();
|
||||
}
|
||||
|
||||
RunAr(aotObjFilePaths, libAotFilePath);
|
||||
|
||||
// Archive the AOT object files into a static library
|
||||
|
||||
File.WriteAllText(Path.Combine(libAotXcFrameworkPath, "Info.plist"),
|
||||
$@"<?xml version=""1.0"" encoding=""UTF-8""?>
|
||||
<!DOCTYPE plist PUBLIC ""-//Apple//DTD PLIST 1.0//EN"" ""http://www.apple.com/DTDs/PropertyList-1.0.dtd"">
|
||||
<plist version=""1.0"">
|
||||
<dict>
|
||||
<key>AvailableLibraries</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>LibraryIdentifier</key>
|
||||
<string>ios-arm64</string>
|
||||
<key>LibraryPath</key>
|
||||
<string>{libAotFileName}</string>
|
||||
<key>SupportedArchitectures</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>SupportedPlatform</key>
|
||||
<string>ios</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>LibraryIdentifier</key>
|
||||
<string>ios-arm64_x86_64-simulator</string>
|
||||
<key>LibraryPath</key>
|
||||
<string>{libAotFileName}</string>
|
||||
<key>SupportedArchitectures</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
<string>x86_64</string>
|
||||
</array>
|
||||
<key>SupportedPlatform</key>
|
||||
<string>ios</string>
|
||||
<key>SupportedPlatformVariant</key>
|
||||
<string>simulator</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XFWK</string>
|
||||
<key>XCFrameworkFormatVersion</key>
|
||||
<string>1.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
");
|
||||
|
||||
// Add the fat AOT static library to the Xcode project
|
||||
CreateDummyLibForSimulator(libAotName, libAotXcFrameworkPath);
|
||||
exporter.AddIosProjectStaticLib(libAotXcFrameworkPath);
|
||||
|
||||
// Generate driver code
|
||||
cppCode.AppendLine("#include <TargetConditionals.h>");
|
||||
|
||||
cppCode.AppendLine("#if !TARGET_OS_SIMULATOR");
|
||||
cppCode.AppendLine("extern \"C\" {");
|
||||
cppCode.AppendLine("// Mono API");
|
||||
cppCode.AppendLine(@"
|
||||
typedef enum {
|
||||
MONO_AOT_MODE_NONE,
|
||||
MONO_AOT_MODE_NORMAL,
|
||||
MONO_AOT_MODE_HYBRID,
|
||||
MONO_AOT_MODE_FULL,
|
||||
MONO_AOT_MODE_LLVMONLY,
|
||||
MONO_AOT_MODE_INTERP,
|
||||
MONO_AOT_MODE_INTERP_LLVMONLY,
|
||||
MONO_AOT_MODE_LLVMONLY_INTERP,
|
||||
MONO_AOT_MODE_LAST = 1000,
|
||||
} MonoAotMode;");
|
||||
cppCode.AppendLine("void mono_jit_set_aot_mode(MonoAotMode);");
|
||||
cppCode.AppendLine("void mono_aot_register_module(void *);");
|
||||
|
||||
if (aotOpts.UseInterpreter)
|
||||
{
|
||||
cppCode.AppendLine("void mono_ee_interp_init(const char *);");
|
||||
cppCode.AppendLine("void mono_icall_table_init();");
|
||||
cppCode.AppendLine("void mono_marshal_ilgen_init();");
|
||||
cppCode.AppendLine("void mono_method_builder_ilgen_init();");
|
||||
cppCode.AppendLine("void mono_sgen_mono_ilgen_init();");
|
||||
}
|
||||
|
||||
foreach (string symbol in aotModuleInfoSymbols)
|
||||
cppCode.AppendLine($"extern void *{symbol};");
|
||||
|
||||
cppCode.AppendLine("void gd_mono_setup_aot() {");
|
||||
|
||||
foreach (string symbol in aotModuleInfoSymbols)
|
||||
cppCode.AppendLine($"\tmono_aot_register_module({symbol});");
|
||||
|
||||
if (aotOpts.UseInterpreter)
|
||||
{
|
||||
cppCode.AppendLine("\tmono_icall_table_init();");
|
||||
cppCode.AppendLine("\tmono_marshal_ilgen_init();");
|
||||
cppCode.AppendLine("\tmono_method_builder_ilgen_init();");
|
||||
cppCode.AppendLine("\tmono_sgen_mono_ilgen_init();");
|
||||
cppCode.AppendLine("\tmono_ee_interp_init(0);");
|
||||
}
|
||||
|
||||
string aotModeStr = null;
|
||||
|
||||
if (aotOpts.LLVMOnly)
|
||||
{
|
||||
aotModeStr = "MONO_AOT_MODE_LLVMONLY"; // --aot=llvmonly
|
||||
}
|
||||
else
|
||||
{
|
||||
if (aotOpts.UseInterpreter)
|
||||
aotModeStr = "MONO_AOT_MODE_INTERP"; // --aot=interp or --aot=interp,full
|
||||
else if (aotOpts.FullAot)
|
||||
aotModeStr = "MONO_AOT_MODE_FULL"; // --aot=full
|
||||
}
|
||||
|
||||
// One of the options above is always set for iOS
|
||||
Debug.Assert(aotModeStr != null);
|
||||
|
||||
cppCode.AppendLine($"\tmono_jit_set_aot_mode({aotModeStr});");
|
||||
|
||||
cppCode.AppendLine("} // gd_mono_setup_aot");
|
||||
|
||||
// Prevent symbols from being stripped
|
||||
|
||||
var symbols = CollectSymbols(assemblies);
|
||||
|
||||
foreach (string symbol in symbols)
|
||||
{
|
||||
cppCode.Append("extern void *");
|
||||
cppCode.Append(symbol);
|
||||
cppCode.AppendLine(";");
|
||||
}
|
||||
|
||||
cppCode.AppendLine("__attribute__((used)) __attribute__((optnone)) static void __godot_symbol_referencer() {");
|
||||
cppCode.AppendLine("\tvoid *aux;");
|
||||
|
||||
foreach (string symbol in symbols)
|
||||
{
|
||||
cppCode.Append("\taux = ");
|
||||
cppCode.Append(symbol);
|
||||
cppCode.AppendLine(";");
|
||||
}
|
||||
|
||||
cppCode.AppendLine("} // __godot_symbol_referencer");
|
||||
|
||||
cppCode.AppendLine("} // extern \"C\"");
|
||||
cppCode.AppendLine("#endif // !TARGET_OS_SIMULATOR");
|
||||
|
||||
// Add the driver code to the Xcode project
|
||||
exporter.AddIosCppCode(cppCode.ToString());
|
||||
|
||||
// Add the required Mono libraries to the Xcode project
|
||||
|
||||
string MonoLibFile(string libFileName) => libFileName + ".iphone.fat.a";
|
||||
|
||||
string MonoLibFromTemplate(string libFileName) =>
|
||||
Path.Combine(Internal.FullTemplatesDir, "iphone-mono-libs", MonoLibFile(libFileName));
|
||||
|
||||
string MonoFrameworkFile(string frameworkFileName) => frameworkFileName + ".xcframework";
|
||||
|
||||
string MonoFrameworkFromTemplate(string frameworkFileName) =>
|
||||
Path.Combine(Internal.FullTemplatesDir, "iphone-mono-libs", MonoFrameworkFile(frameworkFileName));
|
||||
|
||||
exporter.AddIosProjectStaticLib(MonoFrameworkFromTemplate("libmonosgen-2.0"));
|
||||
|
||||
exporter.AddIosProjectStaticLib(MonoFrameworkFromTemplate("libmono-native"));
|
||||
|
||||
if (aotOpts.UseInterpreter)
|
||||
{
|
||||
CreateDummyLibForSimulator("libmono-ee-interp");
|
||||
exporter.AddIosProjectStaticLib(MonoFrameworkFromTemplate("libmono-ee-interp"));
|
||||
CreateDummyLibForSimulator("libmono-icall-table");
|
||||
exporter.AddIosProjectStaticLib(MonoFrameworkFromTemplate("libmono-icall-table"));
|
||||
CreateDummyLibForSimulator("libmono-ilgen");
|
||||
exporter.AddIosProjectStaticLib(MonoFrameworkFromTemplate("libmono-ilgen"));
|
||||
}
|
||||
|
||||
// TODO: Turn into an exporter option
|
||||
bool enableProfiling = false;
|
||||
if (enableProfiling)
|
||||
exporter.AddIosProjectStaticLib(MonoLibFromTemplate("libmono-profiler-log"));
|
||||
|
||||
// Add frameworks required by Mono to the Xcode project
|
||||
exporter.AddIosFramework("libiconv.tbd");
|
||||
exporter.AddIosFramework("GSS.framework");
|
||||
exporter.AddIosFramework("CFNetwork.framework");
|
||||
if (!aotOpts.UseInterpreter)
|
||||
exporter.AddIosFramework("SystemConfiguration.framework");
|
||||
}
|
||||
|
||||
private static List<string> CollectSymbols(IDictionary<string, string> assemblies)
|
||||
{
|
||||
var symbols = new List<string>();
|
||||
|
||||
var resolver = new DefaultAssemblyResolver();
|
||||
foreach (var searchDir in resolver.GetSearchDirectories())
|
||||
resolver.RemoveSearchDirectory(searchDir);
|
||||
foreach (var searchDir in assemblies
|
||||
.Select(a => a.Value.GetBaseDir().NormalizePath()).Distinct())
|
||||
{
|
||||
resolver.AddSearchDirectory(searchDir);
|
||||
}
|
||||
|
||||
AssemblyDefinition ReadAssembly(string fileName)
|
||||
=> AssemblyDefinition.ReadAssembly(fileName,
|
||||
new ReaderParameters {AssemblyResolver = resolver});
|
||||
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
using (var assemblyDef = ReadAssembly(assembly.Value))
|
||||
CollectSymbolsFromAssembly(assemblyDef, symbols);
|
||||
}
|
||||
|
||||
return symbols;
|
||||
}
|
||||
|
||||
private static void CollectSymbolsFromAssembly(AssemblyDefinition assembly, ICollection<string> symbols)
|
||||
{
|
||||
if (!assembly.MainModule.HasTypes)
|
||||
return;
|
||||
|
||||
foreach (var type in assembly.MainModule.Types)
|
||||
{
|
||||
CollectSymbolsFromType(type, symbols);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CollectSymbolsFromType(TypeDefinition type, ICollection<string> symbols)
|
||||
{
|
||||
if (type.HasNestedTypes)
|
||||
{
|
||||
foreach (var nestedType in type.NestedTypes)
|
||||
CollectSymbolsFromType(nestedType, symbols);
|
||||
}
|
||||
|
||||
if (type.Module.HasModuleReferences)
|
||||
CollectPInvokeSymbols(type, symbols);
|
||||
}
|
||||
|
||||
private static void CollectPInvokeSymbols(TypeDefinition type, ICollection<string> symbols)
|
||||
{
|
||||
if (!type.HasMethods)
|
||||
return;
|
||||
|
||||
foreach (var method in type.Methods)
|
||||
{
|
||||
if (!method.IsPInvokeImpl || !method.HasPInvokeInfo)
|
||||
continue;
|
||||
|
||||
var pInvokeInfo = method.PInvokeInfo;
|
||||
|
||||
if (pInvokeInfo == null)
|
||||
continue;
|
||||
|
||||
switch (pInvokeInfo.Module.Name)
|
||||
{
|
||||
case "__Internal":
|
||||
case "libSystem.Net.Security.Native":
|
||||
case "System.Net.Security.Native":
|
||||
case "libSystem.Security.Cryptography.Native.Apple":
|
||||
case "System.Security.Cryptography.Native.Apple":
|
||||
case "libSystem.Native":
|
||||
case "System.Native":
|
||||
case "libSystem.Globalization.Native":
|
||||
case "System.Globalization.Native":
|
||||
{
|
||||
symbols.Add(pInvokeInfo.EntryPoint);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts an assembly name to a valid symbol name in the same way the AOT compiler does
|
||||
private static string AssemblyNameToAotSymbol(string assemblyName)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
|
||||
foreach (char @char in assemblyName)
|
||||
builder.Append(char.IsLetterOrDigit(@char) || @char == '_' ? @char : '_');
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static IEnumerable<string> GetAotCompilerArgs(string platform, bool isDebug, string target, AotOptions aotOpts, string assemblyPath, string outputFilePath)
|
||||
{
|
||||
// TODO: LLVM
|
||||
|
||||
bool aotSoftDebug = isDebug && !aotOpts.EnableLLVM;
|
||||
bool aotDwarfDebug = platform == OS.Platforms.iOS;
|
||||
|
||||
var aotOptions = new List<string>();
|
||||
var optimizerOptions = new List<string>();
|
||||
|
||||
if (aotOpts.LLVMOnly)
|
||||
{
|
||||
aotOptions.Add("llvmonly");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Can be both 'interp' and 'full'
|
||||
if (aotOpts.UseInterpreter)
|
||||
aotOptions.Add("interp");
|
||||
if (aotOpts.FullAot)
|
||||
aotOptions.Add("full");
|
||||
}
|
||||
|
||||
aotOptions.Add(aotSoftDebug ? "soft-debug" : "nodebug");
|
||||
|
||||
if (aotDwarfDebug)
|
||||
aotOptions.Add("dwarfdebug");
|
||||
|
||||
if (platform == OS.Platforms.Android)
|
||||
{
|
||||
string abi = target;
|
||||
|
||||
string androidToolchain = aotOpts.ToolchainPath;
|
||||
|
||||
if (string.IsNullOrEmpty(androidToolchain))
|
||||
{
|
||||
androidToolchain = Path.Combine(GodotSharpDirs.DataEditorToolsDir, "android-toolchains", $"{abi}"); // TODO: $"{abi}-{apiLevel}{(clang?"clang":"")}"
|
||||
|
||||
if (!Directory.Exists(androidToolchain))
|
||||
throw new FileNotFoundException("Missing android toolchain. Specify one in the AOT export settings.");
|
||||
}
|
||||
else if (!Directory.Exists(androidToolchain))
|
||||
{
|
||||
throw new FileNotFoundException("Android toolchain not found: " + androidToolchain);
|
||||
}
|
||||
|
||||
var androidToolPrefixes = new Dictionary<string, string>
|
||||
{
|
||||
["armeabi-v7a"] = "arm-linux-androideabi-",
|
||||
["arm64-v8a"] = "aarch64-linux-android-",
|
||||
["x86"] = "i686-linux-android-",
|
||||
["x86_64"] = "x86_64-linux-android-"
|
||||
};
|
||||
|
||||
aotOptions.Add("tool-prefix=" + Path.Combine(androidToolchain, "bin", androidToolPrefixes[abi]));
|
||||
|
||||
string triple = GetAndroidTriple(abi);
|
||||
aotOptions.Add($"mtriple={triple}");
|
||||
}
|
||||
else if (platform == OS.Platforms.iOS)
|
||||
{
|
||||
if (!aotOpts.LLVMOnly && !aotOpts.UseInterpreter)
|
||||
optimizerOptions.Add("gsharedvt");
|
||||
|
||||
aotOptions.Add("static");
|
||||
|
||||
// I couldn't get the Mono cross-compiler to do assembling, so we'll have to do it ourselves
|
||||
aotOptions.Add("asmonly");
|
||||
|
||||
aotOptions.Add("direct-icalls");
|
||||
|
||||
if (aotSoftDebug)
|
||||
aotOptions.Add("no-direct-calls");
|
||||
|
||||
if (aotOpts.LLVMOnly || !aotOpts.UseInterpreter)
|
||||
aotOptions.Add("direct-pinvoke");
|
||||
|
||||
string arch = target;
|
||||
aotOptions.Add($"mtriple={arch}-ios");
|
||||
}
|
||||
|
||||
aotOptions.Add($"outfile={outputFilePath}");
|
||||
|
||||
if (aotOpts.EnableLLVM)
|
||||
{
|
||||
aotOptions.Add($"llvm-path={aotOpts.LLVMPath}");
|
||||
aotOptions.Add($"llvm-outfile={aotOpts.LLVMOutputPath}");
|
||||
}
|
||||
|
||||
if (aotOpts.ExtraAotOptions.Length > 0)
|
||||
aotOptions.AddRange(aotOpts.ExtraAotOptions);
|
||||
|
||||
if (aotOpts.ExtraOptimizerOptions.Length > 0)
|
||||
optimizerOptions.AddRange(aotOpts.ExtraOptimizerOptions);
|
||||
|
||||
string EscapeOption(string option) => option.Contains(',') ? $"\"{option}\"" : option;
|
||||
string OptionsToString(IEnumerable<string> options) => string.Join(",", options.Select(EscapeOption));
|
||||
|
||||
var runtimeArgs = new List<string>();
|
||||
|
||||
// The '--debug' runtime option is required when using the 'soft-debug' and 'dwarfdebug' AOT options
|
||||
if (aotSoftDebug || aotDwarfDebug)
|
||||
runtimeArgs.Add("--debug");
|
||||
|
||||
if (aotOpts.EnableLLVM)
|
||||
runtimeArgs.Add("--llvm");
|
||||
|
||||
runtimeArgs.Add(aotOptions.Count > 0 ? $"--aot={OptionsToString(aotOptions)}" : "--aot");
|
||||
|
||||
if (optimizerOptions.Count > 0)
|
||||
runtimeArgs.Add($"-O={OptionsToString(optimizerOptions)}");
|
||||
|
||||
runtimeArgs.Add(assemblyPath);
|
||||
|
||||
return runtimeArgs;
|
||||
}
|
||||
|
||||
private static void ExecuteCompiler(string compiler, IEnumerable<string> compilerArgs, string bclDir)
|
||||
{
|
||||
// TODO: Once we move to .NET Standard 2.1 we can use ProcessStartInfo.ArgumentList instead
|
||||
string CmdLineArgsToString(IEnumerable<string> args)
|
||||
{
|
||||
// Not perfect, but as long as we are careful...
|
||||
return string.Join(" ", args.Select(arg => arg.Contains(" ") ? $@"""{arg}""" : arg));
|
||||
}
|
||||
|
||||
using (var process = new Process())
|
||||
{
|
||||
process.StartInfo = new ProcessStartInfo(compiler, CmdLineArgsToString(compilerArgs))
|
||||
{
|
||||
UseShellExecute = false
|
||||
};
|
||||
|
||||
process.StartInfo.EnvironmentVariables.Remove("MONO_ENV_OPTIONS");
|
||||
process.StartInfo.EnvironmentVariables.Remove("MONO_THREADS_SUSPEND");
|
||||
process.StartInfo.EnvironmentVariables.Add("MONO_PATH", bclDir);
|
||||
|
||||
Console.WriteLine($"Running: \"{process.StartInfo.FileName}\" {process.StartInfo.Arguments}");
|
||||
|
||||
if (!process.Start())
|
||||
throw new Exception("Failed to start process for Mono AOT compiler");
|
||||
|
||||
process.WaitForExit();
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
throw new Exception($"Mono AOT compiler exited with code: {process.ExitCode}");
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<string> GetEnablediOSArchs(string[] features)
|
||||
{
|
||||
var iosArchs = new[]
|
||||
{
|
||||
"armv7",
|
||||
"arm64"
|
||||
};
|
||||
|
||||
return iosArchs.Where(features.Contains);
|
||||
}
|
||||
|
||||
private static IEnumerable<string> GetEnabledAndroidAbis(string[] features)
|
||||
{
|
||||
var androidAbis = new[]
|
||||
{
|
||||
"armeabi-v7a",
|
||||
"arm64-v8a",
|
||||
"x86",
|
||||
"x86_64"
|
||||
};
|
||||
|
||||
return androidAbis.Where(features.Contains);
|
||||
}
|
||||
|
||||
private static string GetAndroidTriple(string abi)
|
||||
{
|
||||
var abiArchs = new Dictionary<string, string>
|
||||
{
|
||||
["armeabi-v7a"] = "armv7",
|
||||
["arm64-v8a"] = "aarch64-v8a",
|
||||
["x86"] = "i686",
|
||||
["x86_64"] = "x86_64"
|
||||
};
|
||||
|
||||
string arch = abiArchs[abi];
|
||||
|
||||
return $"{arch}-linux-android";
|
||||
}
|
||||
|
||||
private static string GetMonoCrossDesktopDirName(string platform, string bits)
|
||||
{
|
||||
switch (platform)
|
||||
{
|
||||
case OS.Platforms.Windows:
|
||||
case OS.Platforms.UWP:
|
||||
{
|
||||
string arch = bits == "64" ? "x86_64" : "i686";
|
||||
return $"windows-{arch}";
|
||||
}
|
||||
case OS.Platforms.OSX:
|
||||
{
|
||||
Debug.Assert(bits == null || bits == "64");
|
||||
string arch = "x86_64";
|
||||
return $"{platform}-{arch}";
|
||||
}
|
||||
case OS.Platforms.X11:
|
||||
case OS.Platforms.Server:
|
||||
{
|
||||
string arch = bits == "64" ? "x86_64" : "i686";
|
||||
return $"linux-{arch}";
|
||||
}
|
||||
case OS.Platforms.Haiku:
|
||||
{
|
||||
string arch = bits == "64" ? "x86_64" : "i686";
|
||||
return $"{platform}-{arch}";
|
||||
}
|
||||
default:
|
||||
throw new NotSupportedException($"Platform not supported: {platform}");
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Replace this for a specific path for each platform
|
||||
private static string FindCrossCompiler(string monoCrossBin)
|
||||
{
|
||||
string exeExt = OS.IsWindows ? ".exe" : string.Empty;
|
||||
|
||||
var files = new DirectoryInfo(monoCrossBin).GetFiles($"*mono-sgen{exeExt}", SearchOption.TopDirectoryOnly);
|
||||
if (files.Length > 0)
|
||||
return Path.Combine(monoCrossBin, files[0].Name);
|
||||
|
||||
throw new FileNotFoundException($"Cannot find the mono runtime executable in {monoCrossBin}");
|
||||
}
|
||||
}
|
||||
}
|
144
editor/GodotTools/GodotTools/Export/AotCache.cs
Normal file
144
editor/GodotTools/GodotTools/Export/AotCache.cs
Normal file
@ -0,0 +1,144 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace GodotTools.Export
|
||||
{
|
||||
public class AotCache
|
||||
{
|
||||
private readonly string _cacheFilePath;
|
||||
private readonly Cache _cache = new Cache();
|
||||
private bool _hasUnsavedChanges = false;
|
||||
|
||||
public AotCache(string cacheFilePath)
|
||||
{
|
||||
_cacheFilePath = cacheFilePath;
|
||||
|
||||
if (File.Exists(_cacheFilePath))
|
||||
LoadCache(_cacheFilePath, out _cache);
|
||||
}
|
||||
|
||||
private static byte[] ComputeSha256Checksum(string filePath)
|
||||
{
|
||||
using (var sha256 = SHA256.Create())
|
||||
{
|
||||
using (var streamReader = File.OpenRead(filePath))
|
||||
return sha256.ComputeHash(streamReader);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool CompareHashes(byte[] a, byte[] b)
|
||||
{
|
||||
if (a.Length != b.Length)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < a.Length; i++)
|
||||
{
|
||||
if (a[i] != b[i])
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private class Cache
|
||||
{
|
||||
[JsonProperty("assemblies")]
|
||||
public Dictionary<string, CachedChecksums> Assemblies { get; set; } =
|
||||
new Dictionary<string, CachedChecksums>();
|
||||
}
|
||||
|
||||
private struct CachedChecksums
|
||||
{
|
||||
[JsonProperty("input_checksum")] public string InputChecksumBase64 { get; set; }
|
||||
[JsonProperty("output_checksum")] public string OutputChecksumBase64 { get; set; }
|
||||
}
|
||||
|
||||
private static void LoadCache(string cacheFilePath, out Cache cache)
|
||||
{
|
||||
using (var streamReader = new StreamReader(cacheFilePath, Encoding.UTF8))
|
||||
using (var jsonReader = new JsonTextReader(streamReader))
|
||||
{
|
||||
cache = new JsonSerializer().Deserialize<Cache>(jsonReader);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SaveCache(string cacheFilePath, Cache cache)
|
||||
{
|
||||
using (var streamWriter = new StreamWriter(cacheFilePath, append: false, Encoding.UTF8))
|
||||
using (var jsonWriter = new JsonTextWriter(streamWriter))
|
||||
{
|
||||
new JsonSerializer().Serialize(jsonWriter, cache);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetCachedChecksums(string name, out CachedChecksums cachedChecksums)
|
||||
=> _cache.Assemblies.TryGetValue(name, out cachedChecksums);
|
||||
|
||||
private void ChangeCache(string name, byte[] inputChecksum, byte[] outputChecksum)
|
||||
{
|
||||
_cache.Assemblies[name] = new CachedChecksums()
|
||||
{
|
||||
InputChecksumBase64 = Convert.ToBase64String(inputChecksum),
|
||||
OutputChecksumBase64 = Convert.ToBase64String(outputChecksum)
|
||||
};
|
||||
_hasUnsavedChanges = true;
|
||||
}
|
||||
|
||||
public void SaveCache()
|
||||
{
|
||||
if (!_hasUnsavedChanges)
|
||||
return;
|
||||
SaveCache(_cacheFilePath, _cache);
|
||||
_hasUnsavedChanges = false;
|
||||
}
|
||||
|
||||
private bool IsCached(string name, byte[] inputChecksum, string output)
|
||||
{
|
||||
if (!File.Exists(output))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryGetCachedChecksums(name, out var cachedChecksums))
|
||||
return false;
|
||||
|
||||
if (string.IsNullOrEmpty(cachedChecksums.InputChecksumBase64) ||
|
||||
string.IsNullOrEmpty(cachedChecksums.OutputChecksumBase64))
|
||||
return false;
|
||||
|
||||
var cachedInputChecksum = Convert.FromBase64String(cachedChecksums.InputChecksumBase64);
|
||||
|
||||
if (!CompareHashes(inputChecksum, cachedInputChecksum))
|
||||
return false;
|
||||
|
||||
var outputChecksum = ComputeSha256Checksum(output);
|
||||
var cachedOutputChecksum = Convert.FromBase64String(cachedChecksums.OutputChecksumBase64);
|
||||
|
||||
if (!CompareHashes(outputChecksum, cachedOutputChecksum))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void RunCached(string name, string input, string output, Action action)
|
||||
{
|
||||
var inputChecksum = ComputeSha256Checksum(input);
|
||||
|
||||
if (IsCached(name, inputChecksum, output))
|
||||
{
|
||||
Console.WriteLine($"AOT compiler cache: '{name}' already compiled.");
|
||||
return;
|
||||
}
|
||||
|
||||
action();
|
||||
|
||||
var outputChecksum = ComputeSha256Checksum(output);
|
||||
|
||||
ChangeCache(name, inputChecksum, outputChecksum);
|
||||
}
|
||||
}
|
||||
}
|
485
editor/GodotTools/GodotTools/Export/ExportPlugin.cs
Normal file
485
editor/GodotTools/GodotTools/Export/ExportPlugin.cs
Normal file
@ -0,0 +1,485 @@
|
||||
using Godot;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using GodotTools.Build;
|
||||
using GodotTools.Core;
|
||||
using GodotTools.Internals;
|
||||
using JetBrains.Annotations;
|
||||
using static GodotTools.Internals.Globals;
|
||||
using Directory = GodotTools.Utils.Directory;
|
||||
using File = GodotTools.Utils.File;
|
||||
using OS = GodotTools.Utils.OS;
|
||||
using Path = System.IO.Path;
|
||||
|
||||
namespace GodotTools.Export
|
||||
{
|
||||
public class ExportPlugin : EditorExportPlugin
|
||||
{
|
||||
[Flags]
|
||||
private enum I18NCodesets
|
||||
{
|
||||
None = 0,
|
||||
CJK = 1,
|
||||
MidEast = 2,
|
||||
Other = 4,
|
||||
Rare = 8,
|
||||
West = 16,
|
||||
All = CJK | MidEast | Other | Rare | West
|
||||
}
|
||||
|
||||
private string _maybeLastExportError;
|
||||
|
||||
private void AddI18NAssemblies(Godot.Collections.Dictionary<string, string> assemblies, string bclDir)
|
||||
{
|
||||
var codesets = (I18NCodesets)ProjectSettings.GetSetting("mono/export/i18n_codesets");
|
||||
|
||||
if (codesets == I18NCodesets.None)
|
||||
return;
|
||||
|
||||
void AddI18NAssembly(string name) => assemblies.Add(name, Path.Combine(bclDir, $"{name}.dll"));
|
||||
|
||||
AddI18NAssembly("I18N");
|
||||
|
||||
if ((codesets & I18NCodesets.CJK) != 0)
|
||||
AddI18NAssembly("I18N.CJK");
|
||||
if ((codesets & I18NCodesets.MidEast) != 0)
|
||||
AddI18NAssembly("I18N.MidEast");
|
||||
if ((codesets & I18NCodesets.Other) != 0)
|
||||
AddI18NAssembly("I18N.Other");
|
||||
if ((codesets & I18NCodesets.Rare) != 0)
|
||||
AddI18NAssembly("I18N.Rare");
|
||||
if ((codesets & I18NCodesets.West) != 0)
|
||||
AddI18NAssembly("I18N.West");
|
||||
}
|
||||
|
||||
public void RegisterExportSettings()
|
||||
{
|
||||
// TODO: These would be better as export preset options, but that doesn't seem to be supported yet
|
||||
|
||||
GlobalDef("mono/export/include_scripts_content", false);
|
||||
GlobalDef("mono/export/export_assemblies_inside_pck", true);
|
||||
|
||||
GlobalDef("mono/export/i18n_codesets", I18NCodesets.All);
|
||||
|
||||
ProjectSettings.AddPropertyInfo(new Godot.Collections.Dictionary
|
||||
{
|
||||
["type"] = Variant.Type.Int,
|
||||
["name"] = "mono/export/i18n_codesets",
|
||||
["hint"] = PropertyHint.Flags,
|
||||
["hint_string"] = "CJK,MidEast,Other,Rare,West"
|
||||
});
|
||||
|
||||
GlobalDef("mono/export/aot/enabled", false);
|
||||
GlobalDef("mono/export/aot/full_aot", false);
|
||||
GlobalDef("mono/export/aot/use_interpreter", true);
|
||||
|
||||
// --aot or --aot=opt1,opt2 (use 'mono --aot=help AuxAssembly.dll' to list AOT options)
|
||||
GlobalDef("mono/export/aot/extra_aot_options", Array.Empty<string>());
|
||||
// --optimize/-O=opt1,opt2 (use 'mono --list-opt'' to list optimize options)
|
||||
GlobalDef("mono/export/aot/extra_optimizer_options", Array.Empty<string>());
|
||||
|
||||
GlobalDef("mono/export/aot/android_toolchain_path", "");
|
||||
}
|
||||
|
||||
private void AddFile(string srcPath, string dstPath, bool remap = false)
|
||||
{
|
||||
// Add file to the PCK
|
||||
AddFile(dstPath.Replace("\\", "/"), File.ReadAllBytes(srcPath), remap);
|
||||
}
|
||||
|
||||
// With this method we can override how a file is exported in the PCK
|
||||
public override void _ExportFile(string path, string type, string[] features)
|
||||
{
|
||||
base._ExportFile(path, type, features);
|
||||
|
||||
if (type != Internal.CSharpLanguageType)
|
||||
return;
|
||||
|
||||
if (Path.GetExtension(path) != Internal.CSharpLanguageExtension)
|
||||
throw new ArgumentException($"Resource of type {Internal.CSharpLanguageType} has an invalid file extension: {path}", nameof(path));
|
||||
|
||||
// TODO What if the source file is not part of the game's C# project
|
||||
|
||||
bool includeScriptsContent = (bool)ProjectSettings.GetSetting("mono/export/include_scripts_content");
|
||||
|
||||
if (!includeScriptsContent)
|
||||
{
|
||||
// We don't want to include the source code on exported games.
|
||||
|
||||
// Sadly, Godot prints errors when adding an empty file (nothing goes wrong, it's just noise).
|
||||
// Because of this, we add a file which contains a line break.
|
||||
AddFile(path, System.Text.Encoding.UTF8.GetBytes("\n"), remap: false);
|
||||
|
||||
// Tell the Godot exporter that we already took care of the file
|
||||
Skip();
|
||||
}
|
||||
}
|
||||
|
||||
public override void _ExportBegin(string[] features, bool isDebug, string path, int flags)
|
||||
{
|
||||
base._ExportBegin(features, isDebug, path, flags);
|
||||
|
||||
try
|
||||
{
|
||||
_ExportBeginImpl(features, isDebug, path, flags);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_maybeLastExportError = e.Message;
|
||||
|
||||
// 'maybeLastExportError' cannot be null or empty if there was an error, so we
|
||||
// must consider the possibility of exceptions being thrown without a message.
|
||||
if (string.IsNullOrEmpty(_maybeLastExportError))
|
||||
_maybeLastExportError = $"Exception thrown: {e.GetType().Name}";
|
||||
|
||||
GD.PushError($"Failed to export project: {_maybeLastExportError}");
|
||||
Console.Error.WriteLine(e);
|
||||
// TODO: Do something on error once _ExportBegin supports failing.
|
||||
}
|
||||
}
|
||||
|
||||
private void _ExportBeginImpl(string[] features, bool isDebug, string path, int flags)
|
||||
{
|
||||
_ = flags; // Unused
|
||||
|
||||
if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
|
||||
return;
|
||||
|
||||
if (!DeterminePlatformFromFeatures(features, out string platform))
|
||||
throw new NotSupportedException("Target platform not supported");
|
||||
|
||||
string outputDir = new FileInfo(path).Directory?.FullName ??
|
||||
throw new FileNotFoundException("Base directory not found");
|
||||
|
||||
string buildConfig = isDebug ? "ExportDebug" : "ExportRelease";
|
||||
|
||||
string scriptsMetadataPath = BuildManager.GenerateExportedGameScriptMetadata(isDebug);
|
||||
AddFile(scriptsMetadataPath, scriptsMetadataPath);
|
||||
|
||||
if (!BuildManager.BuildProjectBlocking(buildConfig, platform: platform))
|
||||
throw new Exception("Failed to build project");
|
||||
|
||||
// Add dependency assemblies
|
||||
|
||||
var assemblies = new Godot.Collections.Dictionary<string, string>();
|
||||
|
||||
string projectDllName = GodotSharpDirs.ProjectAssemblyName;
|
||||
string projectDllSrcDir = Path.Combine(GodotSharpDirs.ResTempAssembliesBaseDir, buildConfig);
|
||||
string projectDllSrcPath = Path.Combine(projectDllSrcDir, $"{projectDllName}.dll");
|
||||
|
||||
assemblies[projectDllName] = projectDllSrcPath;
|
||||
|
||||
string bclDir = DeterminePlatformBclDir(platform);
|
||||
|
||||
if (platform == OS.Platforms.Android)
|
||||
{
|
||||
string godotAndroidExtProfileDir = GetBclProfileDir("godot_android_ext");
|
||||
string monoAndroidAssemblyPath = Path.Combine(godotAndroidExtProfileDir, "Mono.Android.dll");
|
||||
|
||||
if (!File.Exists(monoAndroidAssemblyPath))
|
||||
throw new FileNotFoundException("Assembly not found: 'Mono.Android'", monoAndroidAssemblyPath);
|
||||
|
||||
assemblies["Mono.Android"] = monoAndroidAssemblyPath;
|
||||
}
|
||||
else if (platform == OS.Platforms.HTML5)
|
||||
{
|
||||
// Ideally these would be added automatically since they're referenced by the wasm BCL assemblies.
|
||||
// However, at least in the case of 'WebAssembly.Net.Http' for some reason the BCL assemblies
|
||||
// reference a different version even though the assembly is the same, for some weird reason.
|
||||
|
||||
var wasmFrameworkAssemblies = new[] { "WebAssembly.Bindings", "WebAssembly.Net.WebSockets" };
|
||||
|
||||
foreach (string thisWasmFrameworkAssemblyName in wasmFrameworkAssemblies)
|
||||
{
|
||||
string thisWasmFrameworkAssemblyPath = Path.Combine(bclDir, thisWasmFrameworkAssemblyName + ".dll");
|
||||
if (!File.Exists(thisWasmFrameworkAssemblyPath))
|
||||
throw new FileNotFoundException($"Assembly not found: '{thisWasmFrameworkAssemblyName}'", thisWasmFrameworkAssemblyPath);
|
||||
assemblies[thisWasmFrameworkAssemblyName] = thisWasmFrameworkAssemblyPath;
|
||||
}
|
||||
|
||||
// Assemblies that can have a different name in a newer version. Newer version must come first and it has priority.
|
||||
(string newName, string oldName)[] wasmFrameworkAssembliesOneOf = new[]
|
||||
{
|
||||
("System.Net.Http.WebAssemblyHttpHandler", "WebAssembly.Net.Http")
|
||||
};
|
||||
|
||||
foreach (var thisWasmFrameworkAssemblyName in wasmFrameworkAssembliesOneOf)
|
||||
{
|
||||
string thisWasmFrameworkAssemblyPath = Path.Combine(bclDir, thisWasmFrameworkAssemblyName.newName + ".dll");
|
||||
if (File.Exists(thisWasmFrameworkAssemblyPath))
|
||||
{
|
||||
assemblies[thisWasmFrameworkAssemblyName.newName] = thisWasmFrameworkAssemblyPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
thisWasmFrameworkAssemblyPath = Path.Combine(bclDir, thisWasmFrameworkAssemblyName.oldName + ".dll");
|
||||
if (!File.Exists(thisWasmFrameworkAssemblyPath))
|
||||
{
|
||||
throw new FileNotFoundException("Expected one of the following assemblies but none were found: " +
|
||||
$"'{thisWasmFrameworkAssemblyName.newName}' / '{thisWasmFrameworkAssemblyName.oldName}'",
|
||||
thisWasmFrameworkAssemblyPath);
|
||||
}
|
||||
|
||||
assemblies[thisWasmFrameworkAssemblyName.oldName] = thisWasmFrameworkAssemblyPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var initialAssemblies = assemblies.Duplicate();
|
||||
internal_GetExportedAssemblyDependencies(initialAssemblies, buildConfig, bclDir, assemblies);
|
||||
|
||||
AddI18NAssemblies(assemblies, bclDir);
|
||||
|
||||
string outputDataDir = null;
|
||||
|
||||
if (PlatformHasTemplateDir(platform))
|
||||
outputDataDir = ExportDataDirectory(features, platform, isDebug, outputDir);
|
||||
|
||||
string apiConfig = isDebug ? "Debug" : "Release";
|
||||
string resAssembliesDir = Path.Combine(GodotSharpDirs.ResAssembliesBaseDir, apiConfig);
|
||||
|
||||
bool assembliesInsidePck = (bool)ProjectSettings.GetSetting("mono/export/export_assemblies_inside_pck") || outputDataDir == null;
|
||||
|
||||
if (!assembliesInsidePck)
|
||||
{
|
||||
string outputDataGameAssembliesDir = Path.Combine(outputDataDir, "Assemblies");
|
||||
if (!Directory.Exists(outputDataGameAssembliesDir))
|
||||
Directory.CreateDirectory(outputDataGameAssembliesDir);
|
||||
}
|
||||
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
void AddToAssembliesDir(string fileSrcPath)
|
||||
{
|
||||
if (assembliesInsidePck)
|
||||
{
|
||||
string fileDstPath = Path.Combine(resAssembliesDir, fileSrcPath.GetFile());
|
||||
AddFile(fileSrcPath, fileDstPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Assert(outputDataDir != null);
|
||||
string fileDstPath = Path.Combine(outputDataDir, "Assemblies", fileSrcPath.GetFile());
|
||||
File.Copy(fileSrcPath, fileDstPath);
|
||||
}
|
||||
}
|
||||
|
||||
string assemblySrcPath = assembly.Value;
|
||||
|
||||
string assemblyPathWithoutExtension = Path.ChangeExtension(assemblySrcPath, null);
|
||||
string pdbSrcPath = assemblyPathWithoutExtension + ".pdb";
|
||||
|
||||
AddToAssembliesDir(assemblySrcPath);
|
||||
|
||||
if (File.Exists(pdbSrcPath))
|
||||
AddToAssembliesDir(pdbSrcPath);
|
||||
}
|
||||
|
||||
// AOT compilation
|
||||
bool aotEnabled = platform == OS.Platforms.iOS || (bool)ProjectSettings.GetSetting("mono/export/aot/enabled");
|
||||
|
||||
if (aotEnabled)
|
||||
{
|
||||
string aotToolchainPath = null;
|
||||
|
||||
if (platform == OS.Platforms.Android)
|
||||
aotToolchainPath = (string)ProjectSettings.GetSetting("mono/export/aot/android_toolchain_path");
|
||||
|
||||
if (aotToolchainPath == string.Empty)
|
||||
aotToolchainPath = null; // Don't risk it being used as current working dir
|
||||
|
||||
// TODO: LLVM settings are hard-coded and disabled for now
|
||||
var aotOpts = new AotOptions
|
||||
{
|
||||
EnableLLVM = false,
|
||||
LLVMOnly = false,
|
||||
LLVMPath = "",
|
||||
LLVMOutputPath = "",
|
||||
FullAot = platform == OS.Platforms.iOS || (bool)(ProjectSettings.GetSetting("mono/export/aot/full_aot") ?? false),
|
||||
UseInterpreter = (bool)ProjectSettings.GetSetting("mono/export/aot/use_interpreter"),
|
||||
ExtraAotOptions = (string[])ProjectSettings.GetSetting("mono/export/aot/extra_aot_options") ?? Array.Empty<string>(),
|
||||
ExtraOptimizerOptions = (string[])ProjectSettings.GetSetting("mono/export/aot/extra_optimizer_options") ?? Array.Empty<string>(),
|
||||
ToolchainPath = aotToolchainPath
|
||||
};
|
||||
|
||||
AotBuilder.CompileAssemblies(this, aotOpts, features, platform, isDebug, bclDir, outputDir, outputDataDir, assemblies);
|
||||
}
|
||||
}
|
||||
|
||||
public override void _ExportEnd()
|
||||
{
|
||||
base._ExportEnd();
|
||||
|
||||
string aotTempDir = Path.Combine(Path.GetTempPath(), $"godot-aot-{Process.GetCurrentProcess().Id}");
|
||||
|
||||
if (Directory.Exists(aotTempDir))
|
||||
Directory.Delete(aotTempDir, recursive: true);
|
||||
|
||||
// TODO: Just a workaround until the export plugins can be made to abort with errors
|
||||
if (!string.IsNullOrEmpty(_maybeLastExportError)) // Check empty as well, because it's set to empty after hot-reloading
|
||||
{
|
||||
string lastExportError = _maybeLastExportError;
|
||||
_maybeLastExportError = null;
|
||||
|
||||
GodotSharpEditor.Instance.ShowErrorDialog(lastExportError, "Failed to export C# project");
|
||||
}
|
||||
}
|
||||
|
||||
[NotNull]
|
||||
private static string ExportDataDirectory(string[] features, string platform, bool isDebug, string outputDir)
|
||||
{
|
||||
string target = isDebug ? "release_debug" : "release";
|
||||
|
||||
// NOTE: Bits is ok for now as all platforms with a data directory only have one or two architectures.
|
||||
// However, this may change in the future if we add arm linux or windows desktop templates.
|
||||
string bits = features.Contains("64") ? "64" : "32";
|
||||
|
||||
string TemplateDirName() => $"data.mono.{platform}.{bits}.{target}";
|
||||
|
||||
string templateDirPath = Path.Combine(Internal.FullTemplatesDir, TemplateDirName());
|
||||
bool validTemplatePathFound = true;
|
||||
|
||||
if (!Directory.Exists(templateDirPath))
|
||||
{
|
||||
validTemplatePathFound = false;
|
||||
|
||||
if (isDebug)
|
||||
{
|
||||
target = "debug"; // Support both 'release_debug' and 'debug' for the template data directory name
|
||||
templateDirPath = Path.Combine(Internal.FullTemplatesDir, TemplateDirName());
|
||||
validTemplatePathFound = true;
|
||||
|
||||
if (!Directory.Exists(templateDirPath))
|
||||
validTemplatePathFound = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!validTemplatePathFound)
|
||||
throw new FileNotFoundException("Data template directory not found", templateDirPath);
|
||||
|
||||
string outputDataDir = Path.Combine(outputDir, DetermineDataDirNameForProject());
|
||||
|
||||
if (Directory.Exists(outputDataDir))
|
||||
Directory.Delete(outputDataDir, recursive: true); // Clean first
|
||||
|
||||
Directory.CreateDirectory(outputDataDir);
|
||||
|
||||
foreach (string dir in Directory.GetDirectories(templateDirPath, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
Directory.CreateDirectory(Path.Combine(outputDataDir, dir.Substring(templateDirPath.Length + 1)));
|
||||
}
|
||||
|
||||
foreach (string file in Directory.GetFiles(templateDirPath, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
File.Copy(file, Path.Combine(outputDataDir, file.Substring(templateDirPath.Length + 1)));
|
||||
}
|
||||
|
||||
return outputDataDir;
|
||||
}
|
||||
|
||||
private static bool PlatformHasTemplateDir(string platform)
|
||||
{
|
||||
// OSX export templates are contained in a zip, so we place our custom template inside it and let Godot do the rest.
|
||||
return !new[] { OS.Platforms.OSX, OS.Platforms.Android, OS.Platforms.iOS, OS.Platforms.HTML5 }.Contains(platform);
|
||||
}
|
||||
|
||||
private static bool DeterminePlatformFromFeatures(IEnumerable<string> features, out string platform)
|
||||
{
|
||||
foreach (var feature in features)
|
||||
{
|
||||
if (OS.PlatformNameMap.TryGetValue(feature, out platform))
|
||||
return true;
|
||||
}
|
||||
|
||||
platform = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string GetBclProfileDir(string profile)
|
||||
{
|
||||
string templatesDir = Internal.FullTemplatesDir;
|
||||
return Path.Combine(templatesDir, "bcl", profile);
|
||||
}
|
||||
|
||||
private static string DeterminePlatformBclDir(string platform)
|
||||
{
|
||||
string templatesDir = Internal.FullTemplatesDir;
|
||||
string platformBclDir = Path.Combine(templatesDir, "bcl", platform);
|
||||
|
||||
if (!File.Exists(Path.Combine(platformBclDir, "mscorlib.dll")))
|
||||
{
|
||||
string profile = DeterminePlatformBclProfile(platform);
|
||||
platformBclDir = Path.Combine(templatesDir, "bcl", profile);
|
||||
|
||||
if (!File.Exists(Path.Combine(platformBclDir, "mscorlib.dll")))
|
||||
{
|
||||
if (PlatformRequiresCustomBcl(platform))
|
||||
throw new FileNotFoundException($"Missing BCL (Base Class Library) for platform: {platform}");
|
||||
|
||||
platformBclDir = typeof(object).Assembly.Location.GetBaseDir(); // Use the one we're running on
|
||||
}
|
||||
}
|
||||
|
||||
return platformBclDir;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the BCL bundled with the Godot editor can be used for the target platform,
|
||||
/// or if it requires a custom BCL that must be distributed with the export templates.
|
||||
/// </summary>
|
||||
private static bool PlatformRequiresCustomBcl(string platform)
|
||||
{
|
||||
if (new[] { OS.Platforms.Android, OS.Platforms.iOS, OS.Platforms.HTML5 }.Contains(platform))
|
||||
return true;
|
||||
|
||||
// The 'net_4_x' BCL is not compatible between Windows and the other platforms.
|
||||
// We use the names 'net_4_x_win' and 'net_4_x' to differentiate between the two.
|
||||
|
||||
bool isWinOrUwp = new[]
|
||||
{
|
||||
OS.Platforms.Windows,
|
||||
OS.Platforms.UWP
|
||||
}.Contains(platform);
|
||||
|
||||
return OS.IsWindows ? !isWinOrUwp : isWinOrUwp;
|
||||
}
|
||||
|
||||
private static string DeterminePlatformBclProfile(string platform)
|
||||
{
|
||||
switch (platform)
|
||||
{
|
||||
case OS.Platforms.Windows:
|
||||
case OS.Platforms.UWP:
|
||||
return "net_4_x_win";
|
||||
case OS.Platforms.OSX:
|
||||
case OS.Platforms.X11:
|
||||
case OS.Platforms.Server:
|
||||
case OS.Platforms.Haiku:
|
||||
return "net_4_x";
|
||||
case OS.Platforms.Android:
|
||||
return "monodroid";
|
||||
case OS.Platforms.iOS:
|
||||
return "monotouch";
|
||||
case OS.Platforms.HTML5:
|
||||
return "wasm";
|
||||
default:
|
||||
throw new NotSupportedException($"Platform not supported: {platform}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string DetermineDataDirNameForProject()
|
||||
{
|
||||
string appName = (string)ProjectSettings.GetSetting("application/config/name");
|
||||
string appNameSafe = appName.ToSafeDirName();
|
||||
return $"data_{appNameSafe}";
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern void internal_GetExportedAssemblyDependencies(Godot.Collections.Dictionary<string, string> initialAssemblies,
|
||||
string buildConfig, string customBclDir, Godot.Collections.Dictionary<string, string> dependencyAssemblies);
|
||||
}
|
||||
}
|
93
editor/GodotTools/GodotTools/Export/XcodeHelper.cs
Normal file
93
editor/GodotTools/GodotTools/Export/XcodeHelper.cs
Normal file
@ -0,0 +1,93 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace GodotTools.Export
|
||||
{
|
||||
public static class XcodeHelper
|
||||
{
|
||||
private static string _XcodePath = null;
|
||||
|
||||
public static string XcodePath
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_XcodePath == null)
|
||||
{
|
||||
_XcodePath = FindXcode();
|
||||
|
||||
if (_XcodePath == null)
|
||||
throw new Exception("Could not find Xcode");
|
||||
}
|
||||
|
||||
return _XcodePath;
|
||||
}
|
||||
}
|
||||
|
||||
private static string FindSelectedXcode()
|
||||
{
|
||||
var outputWrapper = new Godot.Collections.Array();
|
||||
|
||||
int exitCode = Godot.OS.Execute("xcode-select", new string[] { "--print-path" }, blocking: true, output: outputWrapper);
|
||||
|
||||
if (exitCode == 0)
|
||||
{
|
||||
string output = (string)outputWrapper[0];
|
||||
return output.Trim();
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"'xcode-select --print-path' exited with code: {exitCode}");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string FindXcode()
|
||||
{
|
||||
string selectedXcode = FindSelectedXcode();
|
||||
if (selectedXcode != null)
|
||||
{
|
||||
if (Directory.Exists(Path.Combine(selectedXcode, "Contents", "Developer")))
|
||||
return selectedXcode;
|
||||
|
||||
// The path already pointed to Contents/Developer
|
||||
var dirInfo = new DirectoryInfo(selectedXcode);
|
||||
if (dirInfo.Name != "Developer" || dirInfo.Parent.Name != "Contents")
|
||||
{
|
||||
Console.WriteLine(Path.GetDirectoryName(selectedXcode));
|
||||
Console.WriteLine(System.IO.Directory.GetParent(selectedXcode).Name);
|
||||
Console.Error.WriteLine("Unrecognized path for selected Xcode");
|
||||
}
|
||||
else
|
||||
{
|
||||
return System.IO.Path.GetFullPath($"{selectedXcode}/../..");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Error.WriteLine("Could not find the selected Xcode; trying with a hint path");
|
||||
}
|
||||
|
||||
const string XcodeHintPath = "/Applications/Xcode.app";
|
||||
|
||||
if (Directory.Exists(XcodeHintPath))
|
||||
{
|
||||
if (Directory.Exists(Path.Combine(XcodeHintPath, "Contents", "Developer")))
|
||||
return XcodeHintPath;
|
||||
|
||||
Console.Error.WriteLine($"Found Xcode at '{XcodeHintPath}' but it's missing the 'Contents/Developer' sub-directory");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string FindXcodeTool(string toolName)
|
||||
{
|
||||
string XcodeDefaultToolchain = Path.Combine(XcodePath, "Contents", "Developer", "Toolchains", "XcodeDefault.xctoolchain");
|
||||
|
||||
string path = Path.Combine(XcodeDefaultToolchain, "usr", "bin", toolName);
|
||||
if (File.Exists(path))
|
||||
return path;
|
||||
|
||||
throw new FileNotFoundException($"Cannot find Xcode tool: {toolName}");
|
||||
}
|
||||
}
|
||||
}
|
12
editor/GodotTools/GodotTools/ExternalEditorId.cs
Normal file
12
editor/GodotTools/GodotTools/ExternalEditorId.cs
Normal file
@ -0,0 +1,12 @@
|
||||
namespace GodotTools
|
||||
{
|
||||
public enum ExternalEditorId
|
||||
{
|
||||
None,
|
||||
VisualStudio, // TODO (Windows-only)
|
||||
VisualStudioForMac, // Mac-only
|
||||
MonoDevelop,
|
||||
VsCode,
|
||||
Rider
|
||||
}
|
||||
}
|
548
editor/GodotTools/GodotTools/GodotSharpEditor.cs
Normal file
548
editor/GodotTools/GodotTools/GodotSharpEditor.cs
Normal file
@ -0,0 +1,548 @@
|
||||
using Godot;
|
||||
using GodotTools.Core;
|
||||
using GodotTools.Export;
|
||||
using GodotTools.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using GodotTools.Build;
|
||||
using GodotTools.Ides;
|
||||
using GodotTools.Ides.Rider;
|
||||
using GodotTools.Internals;
|
||||
using GodotTools.ProjectEditor;
|
||||
using JetBrains.Annotations;
|
||||
using static GodotTools.Internals.Globals;
|
||||
using File = GodotTools.Utils.File;
|
||||
using OS = GodotTools.Utils.OS;
|
||||
using Path = System.IO.Path;
|
||||
|
||||
namespace GodotTools
|
||||
{
|
||||
public class GodotSharpEditor : EditorPlugin, ISerializationListener
|
||||
{
|
||||
private EditorSettings _editorSettings;
|
||||
|
||||
private PopupMenu _menuPopup;
|
||||
|
||||
private AcceptDialog _errorDialog;
|
||||
|
||||
private ToolButton _bottomPanelBtn;
|
||||
private ToolButton _toolBarButton;
|
||||
|
||||
// TODO Use WeakReference once we have proper serialization.
|
||||
private WeakRef _exportPluginWeak;
|
||||
|
||||
public GodotIdeManager GodotIdeManager { get; private set; }
|
||||
|
||||
public MSBuildPanel MSBuildPanel { get; private set; }
|
||||
|
||||
public bool SkipBuildBeforePlaying { get; set; } = false;
|
||||
|
||||
private bool CreateProjectSolution()
|
||||
{
|
||||
using (var pr = new EditorProgress("create_csharp_solution", "Generating solution...".TTR(), 3))
|
||||
{
|
||||
pr.Step("Generating C# project...".TTR());
|
||||
|
||||
string resourceDir = ProjectSettings.GlobalizePath("res://");
|
||||
|
||||
string path = resourceDir;
|
||||
string name = GodotSharpDirs.ProjectAssemblyName;
|
||||
|
||||
string guid = CsProjOperations.GenerateGameProject(path, name);
|
||||
|
||||
if (guid.Length > 0)
|
||||
{
|
||||
var solution = new DotNetSolution(name)
|
||||
{
|
||||
DirectoryPath = path
|
||||
};
|
||||
|
||||
var projectInfo = new DotNetSolution.ProjectInfo
|
||||
{
|
||||
Guid = guid,
|
||||
PathRelativeToSolution = name + ".csproj",
|
||||
Configs = new List<string> { "Debug", "ExportDebug", "ExportRelease" }
|
||||
};
|
||||
|
||||
solution.AddNewProject(name, projectInfo);
|
||||
|
||||
try
|
||||
{
|
||||
solution.Save();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
ShowErrorDialog("Failed to save solution. Exception message: ".TTR() + e.Message);
|
||||
return false;
|
||||
}
|
||||
|
||||
pr.Step("Updating Godot API assemblies...".TTR());
|
||||
|
||||
string debugApiAssembliesError = Internal.UpdateApiAssembliesFromPrebuilt("Debug");
|
||||
|
||||
if (!string.IsNullOrEmpty(debugApiAssembliesError))
|
||||
{
|
||||
ShowErrorDialog("Failed to update the Godot API assemblies: " + debugApiAssembliesError);
|
||||
return false;
|
||||
}
|
||||
|
||||
string releaseApiAssembliesError = Internal.UpdateApiAssembliesFromPrebuilt("Release");
|
||||
|
||||
if (!string.IsNullOrEmpty(releaseApiAssembliesError))
|
||||
{
|
||||
ShowErrorDialog("Failed to update the Godot API assemblies: " + releaseApiAssembliesError);
|
||||
return false;
|
||||
}
|
||||
|
||||
pr.Step("Done".TTR());
|
||||
|
||||
// Here, after all calls to progress_task_step
|
||||
CallDeferred(nameof(_RemoveCreateSlnMenuOption));
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowErrorDialog("Failed to create C# project.".TTR());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void _RemoveCreateSlnMenuOption()
|
||||
{
|
||||
_menuPopup.RemoveItem(_menuPopup.GetItemIndex((int)MenuOptions.CreateSln));
|
||||
_bottomPanelBtn.Show();
|
||||
_toolBarButton.Show();
|
||||
}
|
||||
|
||||
private void _MenuOptionPressed(MenuOptions id)
|
||||
{
|
||||
switch (id)
|
||||
{
|
||||
case MenuOptions.CreateSln:
|
||||
CreateProjectSolution();
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(id), id, "Invalid menu option");
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildSolutionPressed()
|
||||
{
|
||||
if (!File.Exists(GodotSharpDirs.ProjectSlnPath))
|
||||
{
|
||||
if (!CreateProjectSolution())
|
||||
return; // Failed to create solution
|
||||
}
|
||||
|
||||
Instance.MSBuildPanel.BuildSolution();
|
||||
}
|
||||
|
||||
private void _FileSystemDockFileMoved(string file, string newFile)
|
||||
{
|
||||
if (Path.GetExtension(file) == Internal.CSharpLanguageExtension)
|
||||
{
|
||||
ProjectUtils.RenameItemInProjectChecked(GodotSharpDirs.ProjectCsProjPath, "Compile",
|
||||
ProjectSettings.GlobalizePath(file), ProjectSettings.GlobalizePath(newFile));
|
||||
}
|
||||
}
|
||||
|
||||
private void _FileSystemDockFileRemoved(string file)
|
||||
{
|
||||
if (Path.GetExtension(file) == Internal.CSharpLanguageExtension)
|
||||
{
|
||||
ProjectUtils.RemoveItemFromProjectChecked(GodotSharpDirs.ProjectCsProjPath, "Compile",
|
||||
ProjectSettings.GlobalizePath(file));
|
||||
}
|
||||
}
|
||||
|
||||
private void _FileSystemDockFolderMoved(string oldFolder, string newFolder)
|
||||
{
|
||||
if (File.Exists(GodotSharpDirs.ProjectCsProjPath))
|
||||
{
|
||||
ProjectUtils.RenameItemsToNewFolderInProjectChecked(GodotSharpDirs.ProjectCsProjPath, "Compile",
|
||||
ProjectSettings.GlobalizePath(oldFolder), ProjectSettings.GlobalizePath(newFolder));
|
||||
}
|
||||
}
|
||||
|
||||
private void _FileSystemDockFolderRemoved(string oldFolder)
|
||||
{
|
||||
if (File.Exists(GodotSharpDirs.ProjectCsProjPath))
|
||||
{
|
||||
ProjectUtils.RemoveItemsInFolderFromProjectChecked(GodotSharpDirs.ProjectCsProjPath, "Compile",
|
||||
ProjectSettings.GlobalizePath(oldFolder));
|
||||
}
|
||||
}
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
base._Ready();
|
||||
|
||||
MSBuildPanel.BuildOutputView.Connect(
|
||||
nameof(BuildOutputView.BuildStateChanged), this, nameof(BuildStateChanged));
|
||||
|
||||
var fileSystemDock = GetEditorInterface().GetFileSystemDock();
|
||||
|
||||
fileSystemDock.Connect("files_moved", this, nameof(_FileSystemDockFileMoved));
|
||||
fileSystemDock.Connect("file_removed", this, nameof(_FileSystemDockFileRemoved));
|
||||
fileSystemDock.Connect("folder_moved", this, nameof(_FileSystemDockFolderMoved));
|
||||
fileSystemDock.Connect("folder_removed", this, nameof(_FileSystemDockFolderRemoved));
|
||||
}
|
||||
|
||||
private enum MenuOptions
|
||||
{
|
||||
CreateSln,
|
||||
}
|
||||
|
||||
public void ShowErrorDialog(string message, string title = "Error")
|
||||
{
|
||||
_errorDialog.WindowTitle = title;
|
||||
_errorDialog.DialogText = message;
|
||||
_errorDialog.PopupCenteredMinsize();
|
||||
}
|
||||
|
||||
private static string _vsCodePath = string.Empty;
|
||||
|
||||
private static readonly string[] VsCodeNames =
|
||||
{
|
||||
"code", "code-oss", "vscode", "vscode-oss", "visual-studio-code", "visual-studio-code-oss"
|
||||
};
|
||||
|
||||
[UsedImplicitly]
|
||||
public Error OpenInExternalEditor(Script script, int line, int col)
|
||||
{
|
||||
var editorId = (ExternalEditorId)_editorSettings.GetSetting("mono/editor/external_editor");
|
||||
|
||||
switch (editorId)
|
||||
{
|
||||
case ExternalEditorId.None:
|
||||
// Not an error. Tells the caller to fallback to the global external editor settings or the built-in editor.
|
||||
return Error.Unavailable;
|
||||
case ExternalEditorId.VisualStudio:
|
||||
{
|
||||
string scriptPath = ProjectSettings.GlobalizePath(script.ResourcePath);
|
||||
|
||||
var args = new List<string>
|
||||
{
|
||||
GodotSharpDirs.ProjectSlnPath,
|
||||
line >= 0 ? $"{scriptPath};{line + 1};{col + 1}" : scriptPath
|
||||
};
|
||||
|
||||
string command = Path.Combine(GodotSharpDirs.DataEditorToolsDir, "GodotTools.OpenVisualStudio.exe");
|
||||
|
||||
try
|
||||
{
|
||||
if (Godot.OS.IsStdoutVerbose())
|
||||
Console.WriteLine($"Running: \"{command}\" {string.Join(" ", args.Select(a => $"\"{a}\""))}");
|
||||
|
||||
OS.RunProcess(command, args);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
GD.PushError($"Error when trying to run code editor: VisualStudio. Exception message: '{e.Message}'");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case ExternalEditorId.VisualStudioForMac:
|
||||
goto case ExternalEditorId.MonoDevelop;
|
||||
case ExternalEditorId.Rider:
|
||||
{
|
||||
string scriptPath = ProjectSettings.GlobalizePath(script.ResourcePath);
|
||||
RiderPathManager.OpenFile(GodotSharpDirs.ProjectSlnPath, scriptPath, line);
|
||||
return Error.Ok;
|
||||
}
|
||||
case ExternalEditorId.MonoDevelop:
|
||||
{
|
||||
string scriptPath = ProjectSettings.GlobalizePath(script.ResourcePath);
|
||||
|
||||
GodotIdeManager.LaunchIdeAsync().ContinueWith(launchTask =>
|
||||
{
|
||||
var editorPick = launchTask.Result;
|
||||
if (line >= 0)
|
||||
editorPick?.SendOpenFile(scriptPath, line + 1, col);
|
||||
else
|
||||
editorPick?.SendOpenFile(scriptPath);
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
case ExternalEditorId.VsCode:
|
||||
{
|
||||
if (string.IsNullOrEmpty(_vsCodePath) || !File.Exists(_vsCodePath))
|
||||
{
|
||||
// Try to search it again if it wasn't found last time or if it was removed from its location
|
||||
_vsCodePath = VsCodeNames.SelectFirstNotNull(OS.PathWhich, orElse: string.Empty);
|
||||
}
|
||||
|
||||
var args = new List<string>();
|
||||
|
||||
bool osxAppBundleInstalled = false;
|
||||
|
||||
if (OS.IsOSX)
|
||||
{
|
||||
// The package path is '/Applications/Visual Studio Code.app'
|
||||
const string vscodeBundleId = "com.microsoft.VSCode";
|
||||
|
||||
osxAppBundleInstalled = Internal.IsOsxAppBundleInstalled(vscodeBundleId);
|
||||
|
||||
if (osxAppBundleInstalled)
|
||||
{
|
||||
args.Add("-b");
|
||||
args.Add(vscodeBundleId);
|
||||
|
||||
// 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.Add("-n");
|
||||
|
||||
// The open process must wait until the application finishes (which is instant in VSCode's case)
|
||||
args.Add("--wait-apps");
|
||||
|
||||
args.Add("--args");
|
||||
}
|
||||
}
|
||||
|
||||
string resourcePath = ProjectSettings.GlobalizePath("res://");
|
||||
args.Add(resourcePath);
|
||||
|
||||
string scriptPath = ProjectSettings.GlobalizePath(script.ResourcePath);
|
||||
|
||||
if (line >= 0)
|
||||
{
|
||||
args.Add("-g");
|
||||
args.Add($"{scriptPath}:{line}:{col}");
|
||||
}
|
||||
else
|
||||
{
|
||||
args.Add(scriptPath);
|
||||
}
|
||||
|
||||
string command;
|
||||
|
||||
if (OS.IsOSX)
|
||||
{
|
||||
if (!osxAppBundleInstalled && string.IsNullOrEmpty(_vsCodePath))
|
||||
{
|
||||
GD.PushError("Cannot find code editor: VSCode");
|
||||
return Error.FileNotFound;
|
||||
}
|
||||
|
||||
command = osxAppBundleInstalled ? "/usr/bin/open" : _vsCodePath;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (string.IsNullOrEmpty(_vsCodePath))
|
||||
{
|
||||
GD.PushError("Cannot find code editor: VSCode");
|
||||
return Error.FileNotFound;
|
||||
}
|
||||
|
||||
command = _vsCodePath;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
OS.RunProcess(command, args);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
GD.PushError($"Error when trying to run code editor: VSCode. Exception message: '{e.Message}'");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
return Error.Ok;
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
public bool OverridesExternalEditor()
|
||||
{
|
||||
return (ExternalEditorId)_editorSettings.GetSetting("mono/editor/external_editor") !=
|
||||
ExternalEditorId.None;
|
||||
}
|
||||
|
||||
public override bool Build()
|
||||
{
|
||||
return BuildManager.EditorBuildCallback();
|
||||
}
|
||||
|
||||
private void ApplyNecessaryChangesToSolution()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Migrate solution from old configuration names to: Debug, ExportDebug and ExportRelease
|
||||
DotNetSolution.MigrateFromOldConfigNames(GodotSharpDirs.ProjectSlnPath);
|
||||
|
||||
var msbuildProject = ProjectUtils.Open(GodotSharpDirs.ProjectCsProjPath)
|
||||
?? throw new Exception("Cannot open C# project");
|
||||
|
||||
// NOTE: The order in which changes are made to the project is important
|
||||
|
||||
// Migrate to MSBuild project Sdks style if using the old style
|
||||
ProjectUtils.MigrateToProjectSdksStyle(msbuildProject, GodotSharpDirs.ProjectAssemblyName);
|
||||
|
||||
ProjectUtils.EnsureGodotSdkIsUpToDate(msbuildProject);
|
||||
|
||||
if (msbuildProject.HasUnsavedChanges)
|
||||
{
|
||||
// Save a copy of the project before replacing it
|
||||
FileUtils.SaveBackupCopy(GodotSharpDirs.ProjectCsProjPath);
|
||||
|
||||
msbuildProject.Save();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
GD.PushError(e.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildStateChanged()
|
||||
{
|
||||
if (_bottomPanelBtn != null)
|
||||
_bottomPanelBtn.Icon = MSBuildPanel.BuildOutputView.BuildStateIcon;
|
||||
}
|
||||
|
||||
public override void EnablePlugin()
|
||||
{
|
||||
base.EnablePlugin();
|
||||
|
||||
if (Instance != null)
|
||||
throw new InvalidOperationException();
|
||||
Instance = this;
|
||||
|
||||
var editorInterface = GetEditorInterface();
|
||||
var editorBaseControl = editorInterface.GetBaseControl();
|
||||
|
||||
_editorSettings = editorInterface.GetEditorSettings();
|
||||
|
||||
_errorDialog = new AcceptDialog();
|
||||
editorBaseControl.AddChild(_errorDialog);
|
||||
|
||||
MSBuildPanel = new MSBuildPanel();
|
||||
_bottomPanelBtn = AddControlToBottomPanel(MSBuildPanel, "MSBuild".TTR());
|
||||
|
||||
AddChild(new HotReloadAssemblyWatcher { Name = "HotReloadAssemblyWatcher" });
|
||||
|
||||
_menuPopup = new PopupMenu();
|
||||
_menuPopup.Hide();
|
||||
_menuPopup.SetAsToplevel(true);
|
||||
|
||||
AddToolSubmenuItem("C#", _menuPopup);
|
||||
|
||||
var buildSolutionShortcut = (ShortCut)EditorShortcut("mono/build_solution");
|
||||
|
||||
_toolBarButton = new ToolButton
|
||||
{
|
||||
Text = "Build",
|
||||
HintTooltip = "Build Solution".TTR(),
|
||||
FocusMode = Control.FocusModeEnum.None,
|
||||
Shortcut = buildSolutionShortcut,
|
||||
ShortcutInTooltip = true
|
||||
};
|
||||
_toolBarButton.Connect("pressed", this, nameof(BuildSolutionPressed));
|
||||
AddControlToContainer(CustomControlContainer.Toolbar, _toolBarButton);
|
||||
|
||||
if (File.Exists(GodotSharpDirs.ProjectSlnPath) && File.Exists(GodotSharpDirs.ProjectCsProjPath))
|
||||
{
|
||||
ApplyNecessaryChangesToSolution();
|
||||
}
|
||||
else
|
||||
{
|
||||
_bottomPanelBtn.Hide();
|
||||
_toolBarButton.Hide();
|
||||
_menuPopup.AddItem("Create C# solution".TTR(), (int)MenuOptions.CreateSln);
|
||||
}
|
||||
|
||||
_menuPopup.Connect("id_pressed", this, nameof(_MenuOptionPressed));
|
||||
|
||||
// External editor settings
|
||||
EditorDef("mono/editor/external_editor", ExternalEditorId.None);
|
||||
|
||||
string settingsHintStr = "Disabled";
|
||||
|
||||
if (OS.IsWindows)
|
||||
{
|
||||
settingsHintStr += $",Visual Studio:{(int)ExternalEditorId.VisualStudio}" +
|
||||
$",MonoDevelop:{(int)ExternalEditorId.MonoDevelop}" +
|
||||
$",Visual Studio Code:{(int)ExternalEditorId.VsCode}" +
|
||||
$",JetBrains Rider:{(int)ExternalEditorId.Rider}";
|
||||
}
|
||||
else if (OS.IsOSX)
|
||||
{
|
||||
settingsHintStr += $",Visual Studio:{(int)ExternalEditorId.VisualStudioForMac}" +
|
||||
$",MonoDevelop:{(int)ExternalEditorId.MonoDevelop}" +
|
||||
$",Visual Studio Code:{(int)ExternalEditorId.VsCode}" +
|
||||
$",JetBrains Rider:{(int)ExternalEditorId.Rider}";
|
||||
}
|
||||
else if (OS.IsUnixLike)
|
||||
{
|
||||
settingsHintStr += $",MonoDevelop:{(int)ExternalEditorId.MonoDevelop}" +
|
||||
$",Visual Studio Code:{(int)ExternalEditorId.VsCode}" +
|
||||
$",JetBrains Rider:{(int)ExternalEditorId.Rider}";
|
||||
}
|
||||
|
||||
_editorSettings.AddPropertyInfo(new Godot.Collections.Dictionary
|
||||
{
|
||||
["type"] = Variant.Type.Int,
|
||||
["name"] = "mono/editor/external_editor",
|
||||
["hint"] = PropertyHint.Enum,
|
||||
["hint_string"] = settingsHintStr
|
||||
});
|
||||
|
||||
// Export plugin
|
||||
var exportPlugin = new ExportPlugin();
|
||||
AddExportPlugin(exportPlugin);
|
||||
exportPlugin.RegisterExportSettings();
|
||||
_exportPluginWeak = WeakRef(exportPlugin);
|
||||
|
||||
BuildManager.Initialize();
|
||||
RiderPathManager.Initialize();
|
||||
|
||||
GodotIdeManager = new GodotIdeManager();
|
||||
AddChild(GodotIdeManager);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (_exportPluginWeak != null)
|
||||
{
|
||||
// We need to dispose our export plugin before the editor destroys EditorSettings.
|
||||
// Otherwise, if the GC disposes it at a later time, EditorExportPlatformAndroid
|
||||
// will be freed after EditorSettings already was, and its device polling thread
|
||||
// will try to access the EditorSettings singleton, resulting in null dereferencing.
|
||||
(_exportPluginWeak.GetRef() as ExportPlugin)?.Dispose();
|
||||
|
||||
_exportPluginWeak.Dispose();
|
||||
}
|
||||
|
||||
GodotIdeManager?.Dispose();
|
||||
}
|
||||
|
||||
public void OnBeforeSerialize()
|
||||
{
|
||||
}
|
||||
|
||||
public void OnAfterDeserialize()
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
// Singleton
|
||||
|
||||
public static GodotSharpEditor Instance { get; private set; }
|
||||
|
||||
[UsedImplicitly]
|
||||
private GodotSharpEditor()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
41
editor/GodotTools/GodotTools/GodotTools.csproj
Normal file
41
editor/GodotTools/GodotTools/GodotTools.csproj
Normal file
@ -0,0 +1,41 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<ProjectGuid>{27B00618-A6F2-4828-B922-05CAEB08C286}</ProjectGuid>
|
||||
<TargetFramework>net472</TargetFramework>
|
||||
<LangVersion>7.2</LangVersion>
|
||||
<!-- The Godot editor uses the Debug Godot API assemblies -->
|
||||
<GodotApiConfiguration>Debug</GodotApiConfiguration>
|
||||
<GodotSourceRootPath>$(SolutionDir)/../../../../</GodotSourceRootPath>
|
||||
<GodotOutputDataDir>$(GodotSourceRootPath)/bin/GodotSharp</GodotOutputDataDir>
|
||||
<GodotApiAssembliesDir>$(GodotOutputDataDir)/Api/$(GodotApiConfiguration)</GodotApiAssembliesDir>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" Exists('$(GodotApiAssembliesDir)/GodotSharp.dll') ">
|
||||
<!-- The project is part of the Godot source tree -->
|
||||
<!-- Use the Godot source tree output folder instead of '$(ProjectDir)/bin' -->
|
||||
<OutputPath>$(GodotOutputDataDir)/Tools</OutputPath>
|
||||
<!-- Must not append '$(TargetFramework)' to the output path in this case -->
|
||||
<AppendTargetFrameworkToOutputPath>False</AppendTargetFrameworkToOutputPath>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="GodotTools.IdeMessaging" Version="1.1.1" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2019.1.3.0" ExcludeAssets="runtime" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="Mono.Cecil" Version="0.11.3" />
|
||||
<Reference Include="GodotSharp">
|
||||
<HintPath>$(GodotApiAssembliesDir)/GodotSharp.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="GodotSharpEditor">
|
||||
<HintPath>$(GodotApiAssembliesDir)/GodotSharpEditor.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\GodotTools.BuildLogger\GodotTools.BuildLogger.csproj" />
|
||||
<ProjectReference Include="..\GodotTools.ProjectEditor\GodotTools.ProjectEditor.csproj" />
|
||||
<ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj" />
|
||||
<!-- Include it if this is an SCons build targeting Windows, or if it's not an SCons build but we're on Windows -->
|
||||
<ProjectReference Include="..\GodotTools.OpenVisualStudio\GodotTools.OpenVisualStudio.csproj" Condition=" '$(GodotPlatform)' == 'windows' Or ( '$(GodotPlatform)' == '' And '$(OS)' == 'Windows_NT' ) " />
|
||||
</ItemGroup>
|
||||
</Project>
|
48
editor/GodotTools/GodotTools/HotReloadAssemblyWatcher.cs
Normal file
48
editor/GodotTools/GodotTools/HotReloadAssemblyWatcher.cs
Normal file
@ -0,0 +1,48 @@
|
||||
using Godot;
|
||||
using GodotTools.Internals;
|
||||
using static GodotTools.Internals.Globals;
|
||||
|
||||
namespace GodotTools
|
||||
{
|
||||
public class HotReloadAssemblyWatcher : Node
|
||||
{
|
||||
private Timer _watchTimer;
|
||||
|
||||
public override void _Notification(int what)
|
||||
{
|
||||
if (what == MainLoop.NotificationWmFocusIn)
|
||||
{
|
||||
RestartTimer();
|
||||
|
||||
if (Internal.IsAssembliesReloadingNeeded())
|
||||
Internal.ReloadAssemblies(softReload: false);
|
||||
}
|
||||
}
|
||||
|
||||
private void TimerTimeout()
|
||||
{
|
||||
if (Internal.IsAssembliesReloadingNeeded())
|
||||
Internal.ReloadAssemblies(softReload: false);
|
||||
}
|
||||
|
||||
public void RestartTimer()
|
||||
{
|
||||
_watchTimer.Stop();
|
||||
_watchTimer.Start();
|
||||
}
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
base._Ready();
|
||||
|
||||
_watchTimer = new Timer
|
||||
{
|
||||
OneShot = false,
|
||||
WaitTime = (float)EditorDef("mono/assembly_watch_interval_sec", 0.5)
|
||||
};
|
||||
_watchTimer.Connect("timeout", this, nameof(TimerTimeout));
|
||||
AddChild(_watchTimer);
|
||||
_watchTimer.Start();
|
||||
}
|
||||
}
|
||||
}
|
229
editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs
Normal file
229
editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs
Normal file
@ -0,0 +1,229 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Godot;
|
||||
using GodotTools.IdeMessaging;
|
||||
using GodotTools.IdeMessaging.Requests;
|
||||
using GodotTools.Internals;
|
||||
|
||||
namespace GodotTools.Ides
|
||||
{
|
||||
public sealed class GodotIdeManager : Node, ISerializationListener
|
||||
{
|
||||
private MessagingServer _messagingServer;
|
||||
|
||||
private MonoDevelop.Instance _monoDevelInstance;
|
||||
private MonoDevelop.Instance _vsForMacInstance;
|
||||
|
||||
private MessagingServer GetRunningOrNewServer()
|
||||
{
|
||||
if (_messagingServer != null && !_messagingServer.IsDisposed)
|
||||
return _messagingServer;
|
||||
|
||||
_messagingServer?.Dispose();
|
||||
_messagingServer = new MessagingServer(OS.GetExecutablePath(), ProjectSettings.GlobalizePath(GodotSharpDirs.ResMetadataDir), new GodotLogger());
|
||||
|
||||
_ = _messagingServer.Listen();
|
||||
|
||||
return _messagingServer;
|
||||
}
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_ = GetRunningOrNewServer();
|
||||
}
|
||||
|
||||
public void OnBeforeSerialize()
|
||||
{
|
||||
}
|
||||
|
||||
public void OnAfterDeserialize()
|
||||
{
|
||||
_ = GetRunningOrNewServer();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_messagingServer?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private string GetExternalEditorIdentity(ExternalEditorId editorId)
|
||||
{
|
||||
// Manually convert to string to avoid breaking compatibility in case we rename the enum fields.
|
||||
switch (editorId)
|
||||
{
|
||||
case ExternalEditorId.None:
|
||||
return null;
|
||||
case ExternalEditorId.VisualStudio:
|
||||
return "VisualStudio";
|
||||
case ExternalEditorId.VsCode:
|
||||
return "VisualStudioCode";
|
||||
case ExternalEditorId.Rider:
|
||||
return "Rider";
|
||||
case ExternalEditorId.VisualStudioForMac:
|
||||
return "VisualStudioForMac";
|
||||
case ExternalEditorId.MonoDevelop:
|
||||
return "MonoDevelop";
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<EditorPick?> LaunchIdeAsync(int millisecondsTimeout = 10000)
|
||||
{
|
||||
var editorId = (ExternalEditorId)GodotSharpEditor.Instance.GetEditorInterface()
|
||||
.GetEditorSettings().GetSetting("mono/editor/external_editor");
|
||||
string editorIdentity = GetExternalEditorIdentity(editorId);
|
||||
|
||||
var runningServer = GetRunningOrNewServer();
|
||||
|
||||
if (runningServer.IsAnyConnected(editorIdentity))
|
||||
return new EditorPick(editorIdentity);
|
||||
|
||||
LaunchIde(editorId, editorIdentity);
|
||||
|
||||
var timeoutTask = Task.Delay(millisecondsTimeout);
|
||||
var completedTask = await Task.WhenAny(timeoutTask, runningServer.AwaitClientConnected(editorIdentity));
|
||||
|
||||
if (completedTask != timeoutTask)
|
||||
return new EditorPick(editorIdentity);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void LaunchIde(ExternalEditorId editorId, string editorIdentity)
|
||||
{
|
||||
switch (editorId)
|
||||
{
|
||||
case ExternalEditorId.None:
|
||||
case ExternalEditorId.VisualStudio:
|
||||
case ExternalEditorId.VsCode:
|
||||
case ExternalEditorId.Rider:
|
||||
throw new NotSupportedException();
|
||||
case ExternalEditorId.VisualStudioForMac:
|
||||
goto case ExternalEditorId.MonoDevelop;
|
||||
case ExternalEditorId.MonoDevelop:
|
||||
{
|
||||
MonoDevelop.Instance GetMonoDevelopInstance(string solutionPath)
|
||||
{
|
||||
if (Utils.OS.IsOSX && editorId == ExternalEditorId.VisualStudioForMac)
|
||||
{
|
||||
_vsForMacInstance = (_vsForMacInstance?.IsDisposed ?? true ? null : _vsForMacInstance) ??
|
||||
new MonoDevelop.Instance(solutionPath, MonoDevelop.EditorId.VisualStudioForMac);
|
||||
return _vsForMacInstance;
|
||||
}
|
||||
|
||||
_monoDevelInstance = (_monoDevelInstance?.IsDisposed ?? true ? null : _monoDevelInstance) ??
|
||||
new MonoDevelop.Instance(solutionPath, MonoDevelop.EditorId.MonoDevelop);
|
||||
return _monoDevelInstance;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var instance = GetMonoDevelopInstance(GodotSharpDirs.ProjectSlnPath);
|
||||
|
||||
if (instance.IsRunning && !GetRunningOrNewServer().IsAnyConnected(editorIdentity))
|
||||
{
|
||||
// After launch we wait up to 30 seconds for the IDE to connect to our messaging server.
|
||||
var waitAfterLaunch = TimeSpan.FromSeconds(30);
|
||||
var timeSinceLaunch = DateTime.Now - instance.LaunchTime;
|
||||
if (timeSinceLaunch > waitAfterLaunch)
|
||||
{
|
||||
instance.Dispose();
|
||||
instance.Execute();
|
||||
}
|
||||
}
|
||||
else if (!instance.IsRunning)
|
||||
{
|
||||
instance.Execute();
|
||||
}
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
string editorName = editorId == ExternalEditorId.VisualStudioForMac ? "Visual Studio" : "MonoDevelop";
|
||||
GD.PushError($"Cannot find code editor: {editorName}");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct EditorPick
|
||||
{
|
||||
private readonly string _identity;
|
||||
|
||||
public EditorPick(string identity)
|
||||
{
|
||||
_identity = identity;
|
||||
}
|
||||
|
||||
public bool IsAnyConnected() =>
|
||||
GodotSharpEditor.Instance.GodotIdeManager.GetRunningOrNewServer().IsAnyConnected(_identity);
|
||||
|
||||
private void SendRequest<TResponse>(Request request)
|
||||
where TResponse : Response, new()
|
||||
{
|
||||
// Logs an error if no client is connected with the specified identity
|
||||
GodotSharpEditor.Instance.GodotIdeManager
|
||||
.GetRunningOrNewServer()
|
||||
.BroadcastRequest<TResponse>(_identity, request);
|
||||
}
|
||||
|
||||
public void SendOpenFile(string file)
|
||||
{
|
||||
SendRequest<OpenFileResponse>(new OpenFileRequest {File = file});
|
||||
}
|
||||
|
||||
public void SendOpenFile(string file, int line)
|
||||
{
|
||||
SendRequest<OpenFileResponse>(new OpenFileRequest {File = file, Line = line});
|
||||
}
|
||||
|
||||
public void SendOpenFile(string file, int line, int column)
|
||||
{
|
||||
SendRequest<OpenFileResponse>(new OpenFileRequest {File = file, Line = line, Column = column});
|
||||
}
|
||||
}
|
||||
|
||||
public EditorPick PickEditor(ExternalEditorId editorId) => new EditorPick(GetExternalEditorIdentity(editorId));
|
||||
|
||||
private class GodotLogger : ILogger
|
||||
{
|
||||
public void LogDebug(string message)
|
||||
{
|
||||
if (OS.IsStdoutVerbose())
|
||||
Console.WriteLine(message);
|
||||
}
|
||||
|
||||
public void LogInfo(string message)
|
||||
{
|
||||
if (OS.IsStdoutVerbose())
|
||||
Console.WriteLine(message);
|
||||
}
|
||||
|
||||
public void LogWarning(string message)
|
||||
{
|
||||
GD.PushWarning(message);
|
||||
}
|
||||
|
||||
public void LogError(string message)
|
||||
{
|
||||
GD.PushError(message);
|
||||
}
|
||||
|
||||
public void LogError(string message, Exception e)
|
||||
{
|
||||
GD.PushError(message + "\n" + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
395
editor/GodotTools/GodotTools/Ides/MessagingServer.cs
Normal file
395
editor/GodotTools/GodotTools/Ides/MessagingServer.cs
Normal file
@ -0,0 +1,395 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GodotTools.IdeMessaging;
|
||||
using GodotTools.IdeMessaging.Requests;
|
||||
using GodotTools.IdeMessaging.Utils;
|
||||
using GodotTools.Internals;
|
||||
using GodotTools.Utils;
|
||||
using Newtonsoft.Json;
|
||||
using Directory = System.IO.Directory;
|
||||
using File = System.IO.File;
|
||||
|
||||
namespace GodotTools.Ides
|
||||
{
|
||||
public sealed class MessagingServer : IDisposable
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private readonly FileStream _metaFile;
|
||||
private string _metaFilePath;
|
||||
|
||||
private readonly SemaphoreSlim _peersSem = new SemaphoreSlim(1);
|
||||
|
||||
private readonly TcpListener _listener;
|
||||
|
||||
private readonly Dictionary<string, Queue<NotifyAwaiter<bool>>> _clientConnectedAwaiters =
|
||||
new Dictionary<string, Queue<NotifyAwaiter<bool>>>();
|
||||
private readonly Dictionary<string, Queue<NotifyAwaiter<bool>>> _clientDisconnectedAwaiters =
|
||||
new Dictionary<string, Queue<NotifyAwaiter<bool>>>();
|
||||
|
||||
public async Task<bool> AwaitClientConnected(string identity)
|
||||
{
|
||||
if (!_clientConnectedAwaiters.TryGetValue(identity, out var queue))
|
||||
{
|
||||
queue = new Queue<NotifyAwaiter<bool>>();
|
||||
_clientConnectedAwaiters.Add(identity, queue);
|
||||
}
|
||||
|
||||
var awaiter = new NotifyAwaiter<bool>();
|
||||
queue.Enqueue(awaiter);
|
||||
return await awaiter;
|
||||
}
|
||||
|
||||
public async Task<bool> AwaitClientDisconnected(string identity)
|
||||
{
|
||||
if (!_clientDisconnectedAwaiters.TryGetValue(identity, out var queue))
|
||||
{
|
||||
queue = new Queue<NotifyAwaiter<bool>>();
|
||||
_clientDisconnectedAwaiters.Add(identity, queue);
|
||||
}
|
||||
|
||||
var awaiter = new NotifyAwaiter<bool>();
|
||||
queue.Enqueue(awaiter);
|
||||
return await awaiter;
|
||||
}
|
||||
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
public bool IsAnyConnected(string identity) => string.IsNullOrEmpty(identity) ?
|
||||
Peers.Count > 0 :
|
||||
Peers.Any(c => c.RemoteIdentity == identity);
|
||||
|
||||
private List<Peer> Peers { get; } = new List<Peer>();
|
||||
|
||||
~MessagingServer()
|
||||
{
|
||||
Dispose(disposing: false);
|
||||
}
|
||||
|
||||
public async void Dispose()
|
||||
{
|
||||
if (IsDisposed)
|
||||
return;
|
||||
|
||||
using (await _peersSem.UseAsync())
|
||||
{
|
||||
if (IsDisposed) // lock may not be fair
|
||||
return;
|
||||
IsDisposed = true;
|
||||
}
|
||||
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
foreach (var connection in Peers)
|
||||
connection.Dispose();
|
||||
Peers.Clear();
|
||||
_listener?.Stop();
|
||||
|
||||
_metaFile?.Dispose();
|
||||
|
||||
File.Delete(_metaFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
public MessagingServer(string editorExecutablePath, string projectMetadataDir, ILogger logger)
|
||||
{
|
||||
this._logger = logger;
|
||||
|
||||
_metaFilePath = Path.Combine(projectMetadataDir, GodotIdeMetadata.DefaultFileName);
|
||||
|
||||
// Make sure the directory exists
|
||||
Directory.CreateDirectory(projectMetadataDir);
|
||||
|
||||
// The Godot editor's file system thread can keep the file open for writing, so we are forced to allow write sharing...
|
||||
const FileShare metaFileShare = FileShare.ReadWrite;
|
||||
|
||||
_metaFile = File.Open(_metaFilePath, FileMode.Create, FileAccess.Write, metaFileShare);
|
||||
|
||||
_listener = new TcpListener(new IPEndPoint(IPAddress.Loopback, port: 0));
|
||||
_listener.Start();
|
||||
|
||||
int port = ((IPEndPoint)_listener.Server.LocalEndPoint).Port;
|
||||
using (var metaFileWriter = new StreamWriter(_metaFile, Encoding.UTF8))
|
||||
{
|
||||
metaFileWriter.WriteLine(port);
|
||||
metaFileWriter.WriteLine(editorExecutablePath);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AcceptClient(TcpClient tcpClient)
|
||||
{
|
||||
_logger.LogDebug("Accept client...");
|
||||
|
||||
using (var peer = new Peer(tcpClient, new ServerHandshake(), new ServerMessageHandler(), _logger))
|
||||
{
|
||||
// ReSharper disable AccessToDisposedClosure
|
||||
peer.Connected += () =>
|
||||
{
|
||||
_logger.LogInfo("Connection open with Ide Client");
|
||||
|
||||
if (_clientConnectedAwaiters.TryGetValue(peer.RemoteIdentity, out var queue))
|
||||
{
|
||||
while (queue.Count > 0)
|
||||
queue.Dequeue().SetResult(true);
|
||||
_clientConnectedAwaiters.Remove(peer.RemoteIdentity);
|
||||
}
|
||||
};
|
||||
|
||||
peer.Disconnected += () =>
|
||||
{
|
||||
if (_clientDisconnectedAwaiters.TryGetValue(peer.RemoteIdentity, out var queue))
|
||||
{
|
||||
while (queue.Count > 0)
|
||||
queue.Dequeue().SetResult(true);
|
||||
_clientDisconnectedAwaiters.Remove(peer.RemoteIdentity);
|
||||
}
|
||||
};
|
||||
// ReSharper restore AccessToDisposedClosure
|
||||
|
||||
try
|
||||
{
|
||||
if (!await peer.DoHandshake("server"))
|
||||
{
|
||||
_logger.LogError("Handshake failed");
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError("Handshake failed with unhandled exception: ", e);
|
||||
return;
|
||||
}
|
||||
|
||||
using (await _peersSem.UseAsync())
|
||||
Peers.Add(peer);
|
||||
|
||||
try
|
||||
{
|
||||
await peer.Process();
|
||||
}
|
||||
finally
|
||||
{
|
||||
using (await _peersSem.UseAsync())
|
||||
Peers.Remove(peer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Listen()
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!IsDisposed)
|
||||
_ = AcceptClient(await _listener.AcceptTcpClientAsync());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (!IsDisposed && !(e is SocketException se && se.SocketErrorCode == SocketError.Interrupted))
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async void BroadcastRequest<TResponse>(string identity, Request request)
|
||||
where TResponse : Response, new()
|
||||
{
|
||||
using (await _peersSem.UseAsync())
|
||||
{
|
||||
if (!IsAnyConnected(identity))
|
||||
{
|
||||
_logger.LogError("Cannot write request. No client connected to the Godot Ide Server.");
|
||||
return;
|
||||
}
|
||||
|
||||
var selectedConnections = string.IsNullOrEmpty(identity) ?
|
||||
Peers :
|
||||
Peers.Where(c => c.RemoteIdentity == identity);
|
||||
|
||||
string body = JsonConvert.SerializeObject(request);
|
||||
|
||||
foreach (var connection in selectedConnections)
|
||||
_ = connection.SendRequest<TResponse>(request.Id, body);
|
||||
}
|
||||
}
|
||||
|
||||
private class ServerHandshake : IHandshake
|
||||
{
|
||||
private static readonly string _serverHandshakeBase =
|
||||
$"{Peer.ServerHandshakeName},Version={Peer.ProtocolVersionMajor}.{Peer.ProtocolVersionMinor}.{Peer.ProtocolVersionRevision}";
|
||||
|
||||
private static readonly string _clientHandshakePattern =
|
||||
$@"{Regex.Escape(Peer.ClientHandshakeName)},Version=([0-9]+)\.([0-9]+)\.([0-9]+),([_a-zA-Z][_a-zA-Z0-9]{{0,63}})";
|
||||
|
||||
public string GetHandshakeLine(string identity) => $"{_serverHandshakeBase},{identity}";
|
||||
|
||||
public bool IsValidPeerHandshake(string handshake, out string identity, ILogger logger)
|
||||
{
|
||||
identity = null;
|
||||
|
||||
var match = Regex.Match(handshake, _clientHandshakePattern);
|
||||
|
||||
if (!match.Success)
|
||||
return false;
|
||||
|
||||
if (!uint.TryParse(match.Groups[1].Value, out uint clientMajor) || Peer.ProtocolVersionMajor != clientMajor)
|
||||
{
|
||||
logger.LogDebug("Incompatible major version: " + match.Groups[1].Value);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
|
||||
if (!uint.TryParse(match.Groups[2].Value, out uint clientMinor) || Peer.ProtocolVersionMinor > clientMinor)
|
||||
{
|
||||
logger.LogDebug("Incompatible minor version: " + match.Groups[2].Value);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!uint.TryParse(match.Groups[3].Value, out uint _)) // Revision
|
||||
{
|
||||
logger.LogDebug("Incompatible revision build: " + match.Groups[3].Value);
|
||||
return false;
|
||||
}
|
||||
|
||||
identity = match.Groups[4].Value;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class ServerMessageHandler : IMessageHandler
|
||||
{
|
||||
private static void DispatchToMainThread(Action action)
|
||||
{
|
||||
var d = new SendOrPostCallback(state => action());
|
||||
Godot.Dispatcher.SynchronizationContext.Post(d, null);
|
||||
}
|
||||
|
||||
private readonly Dictionary<string, Peer.RequestHandler> requestHandlers = InitializeRequestHandlers();
|
||||
|
||||
public async Task<MessageContent> HandleRequest(Peer peer, string id, MessageContent content, ILogger logger)
|
||||
{
|
||||
if (!requestHandlers.TryGetValue(id, out var handler))
|
||||
{
|
||||
logger.LogError($"Received unknown request: {id}");
|
||||
return new MessageContent(MessageStatus.RequestNotSupported, "null");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var response = await handler(peer, content);
|
||||
return new MessageContent(response.Status, JsonConvert.SerializeObject(response));
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
logger.LogError($"Received request with invalid body: {id}");
|
||||
return new MessageContent(MessageStatus.InvalidRequestBody, "null");
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, Peer.RequestHandler> InitializeRequestHandlers()
|
||||
{
|
||||
return new Dictionary<string, Peer.RequestHandler>
|
||||
{
|
||||
[PlayRequest.Id] = async (peer, content) =>
|
||||
{
|
||||
_ = JsonConvert.DeserializeObject<PlayRequest>(content.Body);
|
||||
return await HandlePlay();
|
||||
},
|
||||
[DebugPlayRequest.Id] = async (peer, content) =>
|
||||
{
|
||||
var request = JsonConvert.DeserializeObject<DebugPlayRequest>(content.Body);
|
||||
return await HandleDebugPlay(request);
|
||||
},
|
||||
[StopPlayRequest.Id] = async (peer, content) =>
|
||||
{
|
||||
var request = JsonConvert.DeserializeObject<StopPlayRequest>(content.Body);
|
||||
return await HandleStopPlay(request);
|
||||
},
|
||||
[ReloadScriptsRequest.Id] = async (peer, content) =>
|
||||
{
|
||||
_ = JsonConvert.DeserializeObject<ReloadScriptsRequest>(content.Body);
|
||||
return await HandleReloadScripts();
|
||||
},
|
||||
[CodeCompletionRequest.Id] = async (peer, content) =>
|
||||
{
|
||||
var request = JsonConvert.DeserializeObject<CodeCompletionRequest>(content.Body);
|
||||
return await HandleCodeCompletionRequest(request);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static Task<Response> HandlePlay()
|
||||
{
|
||||
DispatchToMainThread(() =>
|
||||
{
|
||||
// TODO: Add BuildBeforePlaying flag to PlayRequest
|
||||
|
||||
// Run the game
|
||||
Internal.EditorRunPlay();
|
||||
});
|
||||
return Task.FromResult<Response>(new PlayResponse());
|
||||
}
|
||||
|
||||
private static Task<Response> HandleDebugPlay(DebugPlayRequest request)
|
||||
{
|
||||
DispatchToMainThread(() =>
|
||||
{
|
||||
// Tell the build callback whether the editor already built the solution or not
|
||||
GodotSharpEditor.Instance.SkipBuildBeforePlaying = !(request.BuildBeforePlaying ?? true);
|
||||
|
||||
// Pass the debugger agent settings to the player via an environment variables
|
||||
// TODO: It would be better if this was an argument in EditorRunPlay instead
|
||||
Environment.SetEnvironmentVariable("GODOT_MONO_DEBUGGER_AGENT",
|
||||
"--debugger-agent=transport=dt_socket" +
|
||||
$",address={request.DebuggerHost}:{request.DebuggerPort}" +
|
||||
",server=n");
|
||||
|
||||
// Run the game
|
||||
Internal.EditorRunPlay();
|
||||
|
||||
// Restore normal settings
|
||||
Environment.SetEnvironmentVariable("GODOT_MONO_DEBUGGER_AGENT", "");
|
||||
GodotSharpEditor.Instance.SkipBuildBeforePlaying = false;
|
||||
});
|
||||
return Task.FromResult<Response>(new DebugPlayResponse());
|
||||
}
|
||||
|
||||
private static Task<Response> HandleStopPlay(StopPlayRequest request)
|
||||
{
|
||||
DispatchToMainThread(Internal.EditorRunStop);
|
||||
return Task.FromResult<Response>(new StopPlayResponse());
|
||||
}
|
||||
|
||||
private static Task<Response> HandleReloadScripts()
|
||||
{
|
||||
DispatchToMainThread(Internal.ScriptEditorDebugger_ReloadScripts);
|
||||
return Task.FromResult<Response>(new ReloadScriptsResponse());
|
||||
}
|
||||
|
||||
private static async Task<Response> HandleCodeCompletionRequest(CodeCompletionRequest request)
|
||||
{
|
||||
// This is needed if the "resource path" part of the path is case insensitive.
|
||||
// However, it doesn't fix resource loading if the rest of the path is also case insensitive.
|
||||
string scriptFileLocalized = FsPathUtils.LocalizePathWithCaseChecked(request.ScriptFile);
|
||||
|
||||
var response = new CodeCompletionResponse {Kind = request.Kind, ScriptFile = request.ScriptFile};
|
||||
response.Suggestions = await Task.Run(() =>
|
||||
Internal.CodeCompletionRequest(response.Kind, scriptFileLocalized ?? request.ScriptFile));
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
namespace GodotTools.Ides.MonoDevelop
|
||||
{
|
||||
public enum EditorId
|
||||
{
|
||||
MonoDevelop = 0,
|
||||
VisualStudioForMac = 1
|
||||
}
|
||||
}
|
141
editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs
Normal file
141
editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs
Normal file
@ -0,0 +1,141 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using GodotTools.Internals;
|
||||
using GodotTools.Utils;
|
||||
|
||||
namespace GodotTools.Ides.MonoDevelop
|
||||
{
|
||||
public class Instance : IDisposable
|
||||
{
|
||||
public DateTime LaunchTime { get; private set; }
|
||||
private readonly string _solutionFile;
|
||||
private readonly EditorId _editorId;
|
||||
|
||||
private Process _process;
|
||||
|
||||
public bool IsRunning => _process != null && !_process.HasExited;
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
public void Execute()
|
||||
{
|
||||
bool newWindow = _process == null || _process.HasExited;
|
||||
|
||||
var args = new List<string>();
|
||||
|
||||
string command;
|
||||
|
||||
if (OS.IsOSX)
|
||||
{
|
||||
string bundleId = BundleIds[_editorId];
|
||||
|
||||
if (Internal.IsOsxAppBundleInstalled(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 = OS.PathWhich(ExecutableNames[_editorId]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
command = OS.PathWhich(ExecutableNames[_editorId]);
|
||||
}
|
||||
|
||||
args.Add("--ipc-tcp");
|
||||
|
||||
if (newWindow)
|
||||
args.Add("\"" + Path.GetFullPath(_solutionFile) + "\"");
|
||||
|
||||
if (command == null)
|
||||
throw new FileNotFoundException();
|
||||
|
||||
LaunchTime = DateTime.Now;
|
||||
|
||||
if (newWindow)
|
||||
{
|
||||
_process = Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = command,
|
||||
Arguments = string.Join(" ", args),
|
||||
UseShellExecute = true
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = command,
|
||||
Arguments = string.Join(" ", args),
|
||||
UseShellExecute = true
|
||||
})?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public Instance(string solutionFile, EditorId editorId)
|
||||
{
|
||||
if (editorId == EditorId.VisualStudioForMac && !OS.IsOSX)
|
||||
throw new InvalidOperationException($"{nameof(EditorId.VisualStudioForMac)} not supported on this platform");
|
||||
|
||||
_solutionFile = solutionFile;
|
||||
_editorId = editorId;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
IsDisposed = true;
|
||||
_process?.Dispose();
|
||||
}
|
||||
|
||||
private static readonly IReadOnlyDictionary<EditorId, string> ExecutableNames;
|
||||
private static readonly IReadOnlyDictionary<EditorId, string> BundleIds;
|
||||
|
||||
static Instance()
|
||||
{
|
||||
if (OS.IsOSX)
|
||||
{
|
||||
ExecutableNames = new Dictionary<EditorId, string>
|
||||
{
|
||||
// Rely on PATH
|
||||
{EditorId.MonoDevelop, "monodevelop"},
|
||||
{EditorId.VisualStudioForMac, "VisualStudio"}
|
||||
};
|
||||
BundleIds = new Dictionary<EditorId, string>
|
||||
{
|
||||
// TODO EditorId.MonoDevelop
|
||||
{EditorId.VisualStudioForMac, "com.microsoft.visual-studio"}
|
||||
};
|
||||
}
|
||||
else if (OS.IsWindows)
|
||||
{
|
||||
ExecutableNames = 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 (OS.IsUnixLike)
|
||||
{
|
||||
ExecutableNames = new Dictionary<EditorId, string>
|
||||
{
|
||||
// Rely on PATH
|
||||
{EditorId.MonoDevelop, "monodevelop"}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
470
editor/GodotTools/GodotTools/Ides/Rider/RiderPathLocator.cs
Normal file
470
editor/GodotTools/GodotTools/Ides/Rider/RiderPathLocator.cs
Normal file
@ -0,0 +1,470 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Godot;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Win32;
|
||||
using Newtonsoft.Json;
|
||||
using Directory = System.IO.Directory;
|
||||
using Environment = System.Environment;
|
||||
using File = System.IO.File;
|
||||
using Path = System.IO.Path;
|
||||
using OS = GodotTools.Utils.OS;
|
||||
|
||||
// ReSharper disable UnassignedField.Local
|
||||
// ReSharper disable InconsistentNaming
|
||||
// ReSharper disable UnassignedField.Global
|
||||
// ReSharper disable MemberHidesStaticFromOuterClass
|
||||
|
||||
namespace GodotTools.Ides.Rider
|
||||
{
|
||||
/// <summary>
|
||||
/// This code is a modified version of the JetBrains resharper-unity plugin listed under Apache License 2.0 license:
|
||||
/// https://github.com/JetBrains/resharper-unity/blob/master/unity/JetBrains.Rider.Unity.Editor/EditorPlugin/RiderPathLocator.cs
|
||||
/// </summary>
|
||||
public static class RiderPathLocator
|
||||
{
|
||||
public static RiderInfo[] GetAllRiderPaths()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (OS.IsWindows)
|
||||
{
|
||||
return CollectRiderInfosWindows();
|
||||
}
|
||||
if (OS.IsOSX)
|
||||
{
|
||||
return CollectRiderInfosMac();
|
||||
}
|
||||
if (OS.IsUnixLike)
|
||||
{
|
||||
return CollectAllRiderPathsLinux();
|
||||
}
|
||||
throw new Exception("Unexpected OS.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
GD.PushWarning(e.Message);
|
||||
}
|
||||
|
||||
return Array.Empty<RiderInfo>();
|
||||
}
|
||||
|
||||
private static RiderInfo[] CollectAllRiderPathsLinux()
|
||||
{
|
||||
var installInfos = new List<RiderInfo>();
|
||||
string home = Environment.GetEnvironmentVariable("HOME");
|
||||
if (!string.IsNullOrEmpty(home))
|
||||
{
|
||||
string toolboxRiderRootPath = GetToolboxBaseDir();
|
||||
installInfos.AddRange(CollectPathsFromToolbox(toolboxRiderRootPath, "bin", "rider.sh", false)
|
||||
.Select(a => new RiderInfo(a, true)).ToList());
|
||||
|
||||
//$Home/.local/share/applications/jetbrains-rider.desktop
|
||||
var shortcut = new FileInfo(Path.Combine(home, @".local/share/applications/jetbrains-rider.desktop"));
|
||||
|
||||
if (shortcut.Exists)
|
||||
{
|
||||
string[] lines = File.ReadAllLines(shortcut.FullName);
|
||||
foreach (string line in lines)
|
||||
{
|
||||
if (!line.StartsWith("Exec=\""))
|
||||
continue;
|
||||
string path = line.Split('"').Where((item, index) => index == 1).SingleOrDefault();
|
||||
if (string.IsNullOrEmpty(path))
|
||||
continue;
|
||||
|
||||
if (installInfos.Any(a => a.Path == path)) // avoid adding similar build as from toolbox
|
||||
continue;
|
||||
installInfos.Add(new RiderInfo(path, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// snap install
|
||||
string snapInstallPath = "/snap/rider/current/bin/rider.sh";
|
||||
if (new FileInfo(snapInstallPath).Exists)
|
||||
installInfos.Add(new RiderInfo(snapInstallPath, false));
|
||||
|
||||
return installInfos.ToArray();
|
||||
}
|
||||
|
||||
private static RiderInfo[] CollectRiderInfosMac()
|
||||
{
|
||||
var installInfos = new List<RiderInfo>();
|
||||
// "/Applications/*Rider*.app"
|
||||
// should be combined with "Contents/MacOS/rider"
|
||||
var folder = new DirectoryInfo("/Applications");
|
||||
if (folder.Exists)
|
||||
{
|
||||
installInfos.AddRange(folder.GetDirectories("*Rider*.app")
|
||||
.Select(a => new RiderInfo(Path.Combine(a.FullName, "Contents/MacOS/rider"), false))
|
||||
.ToList());
|
||||
}
|
||||
|
||||
// /Users/user/Library/Application Support/JetBrains/Toolbox/apps/Rider/ch-1/181.3870.267/Rider EAP.app
|
||||
// should be combined with "Contents/MacOS/rider"
|
||||
string toolboxRiderRootPath = GetToolboxBaseDir();
|
||||
var paths = CollectPathsFromToolbox(toolboxRiderRootPath, "", "Rider*.app", true)
|
||||
.Select(a => new RiderInfo(Path.Combine(a, "Contents/MacOS/rider"), true));
|
||||
installInfos.AddRange(paths);
|
||||
|
||||
return installInfos.ToArray();
|
||||
}
|
||||
|
||||
private static RiderInfo[] CollectRiderInfosWindows()
|
||||
{
|
||||
var installInfos = new List<RiderInfo>();
|
||||
var toolboxRiderRootPath = GetToolboxBaseDir();
|
||||
var installPathsToolbox = CollectPathsFromToolbox(toolboxRiderRootPath, "bin", "rider64.exe", false).ToList();
|
||||
installInfos.AddRange(installPathsToolbox.Select(a => new RiderInfo(a, true)).ToList());
|
||||
|
||||
var installPaths = new List<string>();
|
||||
const string registryKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall";
|
||||
CollectPathsFromRegistry(registryKey, installPaths);
|
||||
const string wowRegistryKey = @"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall";
|
||||
CollectPathsFromRegistry(wowRegistryKey, installPaths);
|
||||
|
||||
installInfos.AddRange(installPaths.Select(a => new RiderInfo(a, false)).ToList());
|
||||
|
||||
return installInfos.ToArray();
|
||||
}
|
||||
|
||||
private static string GetToolboxBaseDir()
|
||||
{
|
||||
if (OS.IsWindows)
|
||||
{
|
||||
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
return GetToolboxRiderRootPath(localAppData);
|
||||
}
|
||||
|
||||
if (OS.IsOSX)
|
||||
{
|
||||
var home = Environment.GetEnvironmentVariable("HOME");
|
||||
if (string.IsNullOrEmpty(home))
|
||||
return string.Empty;
|
||||
var localAppData = Path.Combine(home, @"Library/Application Support");
|
||||
return GetToolboxRiderRootPath(localAppData);
|
||||
}
|
||||
|
||||
if (OS.IsUnixLike)
|
||||
{
|
||||
var home = Environment.GetEnvironmentVariable("HOME");
|
||||
if (string.IsNullOrEmpty(home))
|
||||
return string.Empty;
|
||||
var localAppData = Path.Combine(home, @".local/share");
|
||||
return GetToolboxRiderRootPath(localAppData);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
|
||||
private static string GetToolboxRiderRootPath(string localAppData)
|
||||
{
|
||||
var toolboxPath = Path.Combine(localAppData, @"JetBrains/Toolbox");
|
||||
var settingsJson = Path.Combine(toolboxPath, ".settings.json");
|
||||
|
||||
if (File.Exists(settingsJson))
|
||||
{
|
||||
var path = SettingsJson.GetInstallLocationFromJson(File.ReadAllText(settingsJson));
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
toolboxPath = path;
|
||||
}
|
||||
|
||||
var toolboxRiderRootPath = Path.Combine(toolboxPath, @"apps/Rider");
|
||||
return toolboxRiderRootPath;
|
||||
}
|
||||
|
||||
internal static ProductInfo GetBuildVersion(string path)
|
||||
{
|
||||
var buildTxtFileInfo = new FileInfo(Path.Combine(path, GetRelativePathToBuildTxt()));
|
||||
var dir = buildTxtFileInfo.DirectoryName;
|
||||
if (!Directory.Exists(dir))
|
||||
return null;
|
||||
var buildVersionFile = new FileInfo(Path.Combine(dir, "product-info.json"));
|
||||
if (!buildVersionFile.Exists)
|
||||
return null;
|
||||
var json = File.ReadAllText(buildVersionFile.FullName);
|
||||
return ProductInfo.GetProductInfo(json);
|
||||
}
|
||||
|
||||
internal static Version GetBuildNumber(string path)
|
||||
{
|
||||
var file = new FileInfo(Path.Combine(path, GetRelativePathToBuildTxt()));
|
||||
if (!file.Exists)
|
||||
return null;
|
||||
var text = File.ReadAllText(file.FullName);
|
||||
if (text.Length <= 3)
|
||||
return null;
|
||||
|
||||
var versionText = text.Substring(3);
|
||||
return Version.TryParse(versionText, out var v) ? v : null;
|
||||
}
|
||||
|
||||
internal static bool IsToolbox(string path)
|
||||
{
|
||||
return path.StartsWith(GetToolboxBaseDir());
|
||||
}
|
||||
|
||||
private static string GetRelativePathToBuildTxt()
|
||||
{
|
||||
if (OS.IsWindows || OS.IsUnixLike)
|
||||
return "../../build.txt";
|
||||
if (OS.IsOSX)
|
||||
return "Contents/Resources/build.txt";
|
||||
throw new Exception("Unknown OS.");
|
||||
}
|
||||
|
||||
private static void CollectPathsFromRegistry(string registryKey, List<string> installPaths)
|
||||
{
|
||||
using (var key = Registry.CurrentUser.OpenSubKey(registryKey))
|
||||
{
|
||||
CollectPathsFromRegistry(installPaths, key);
|
||||
}
|
||||
using (var key = Registry.LocalMachine.OpenSubKey(registryKey))
|
||||
{
|
||||
CollectPathsFromRegistry(installPaths, key);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CollectPathsFromRegistry(List<string> installPaths, RegistryKey key)
|
||||
{
|
||||
if (key == null) return;
|
||||
foreach (var subkeyName in key.GetSubKeyNames().Where(a => a.Contains("Rider")))
|
||||
{
|
||||
using (var subkey = key.OpenSubKey(subkeyName))
|
||||
{
|
||||
var folderObject = subkey?.GetValue("InstallLocation");
|
||||
if (folderObject == null) continue;
|
||||
var folder = folderObject.ToString();
|
||||
var possiblePath = Path.Combine(folder, @"bin\rider64.exe");
|
||||
if (File.Exists(possiblePath))
|
||||
installPaths.Add(possiblePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string[] CollectPathsFromToolbox(string toolboxRiderRootPath, string dirName, string searchPattern,
|
||||
bool isMac)
|
||||
{
|
||||
if (!Directory.Exists(toolboxRiderRootPath))
|
||||
return Array.Empty<string>();
|
||||
|
||||
var channelDirs = Directory.GetDirectories(toolboxRiderRootPath);
|
||||
var paths = channelDirs.SelectMany(channelDir =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// use history.json - last entry stands for the active build https://jetbrains.slack.com/archives/C07KNP99D/p1547807024066500?thread_ts=1547731708.057700&cid=C07KNP99D
|
||||
var historyFile = Path.Combine(channelDir, ".history.json");
|
||||
if (File.Exists(historyFile))
|
||||
{
|
||||
var json = File.ReadAllText(historyFile);
|
||||
var build = ToolboxHistory.GetLatestBuildFromJson(json);
|
||||
if (build != null)
|
||||
{
|
||||
var buildDir = Path.Combine(channelDir, build);
|
||||
var executablePaths = GetExecutablePaths(dirName, searchPattern, isMac, buildDir);
|
||||
if (executablePaths.Any())
|
||||
return executablePaths;
|
||||
}
|
||||
}
|
||||
|
||||
var channelFile = Path.Combine(channelDir, ".channel.settings.json");
|
||||
if (File.Exists(channelFile))
|
||||
{
|
||||
var json = File.ReadAllText(channelFile).Replace("active-application", "active_application");
|
||||
var build = ToolboxInstallData.GetLatestBuildFromJson(json);
|
||||
if (build != null)
|
||||
{
|
||||
var buildDir = Path.Combine(channelDir, build);
|
||||
var executablePaths = GetExecutablePaths(dirName, searchPattern, isMac, buildDir);
|
||||
if (executablePaths.Any())
|
||||
return executablePaths;
|
||||
}
|
||||
}
|
||||
|
||||
// changes in toolbox json files format may brake the logic above, so return all found Rider installations
|
||||
return Directory.GetDirectories(channelDir)
|
||||
.SelectMany(buildDir => GetExecutablePaths(dirName, searchPattern, isMac, buildDir));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// do not write to Debug.Log, just log it.
|
||||
Logger.Warn($"Failed to get RiderPath from {channelDir}", e);
|
||||
}
|
||||
|
||||
return Array.Empty<string>();
|
||||
})
|
||||
.Where(c => !string.IsNullOrEmpty(c))
|
||||
.ToArray();
|
||||
return paths;
|
||||
}
|
||||
|
||||
private static string[] GetExecutablePaths(string dirName, string searchPattern, bool isMac, string buildDir)
|
||||
{
|
||||
var folder = new DirectoryInfo(Path.Combine(buildDir, dirName));
|
||||
if (!folder.Exists)
|
||||
return Array.Empty<string>();
|
||||
|
||||
if (!isMac)
|
||||
return new[] { Path.Combine(folder.FullName, searchPattern) }.Where(File.Exists).ToArray();
|
||||
return folder.GetDirectories(searchPattern).Select(f => f.FullName)
|
||||
.Where(Directory.Exists).ToArray();
|
||||
}
|
||||
|
||||
// Disable the "field is never assigned" compiler warning. We never assign it, but Unity does.
|
||||
// Note that Unity disable this warning in the generated C# projects
|
||||
#pragma warning disable 0649
|
||||
|
||||
[Serializable]
|
||||
class SettingsJson
|
||||
{
|
||||
public string install_location;
|
||||
|
||||
[CanBeNull]
|
||||
public static string GetInstallLocationFromJson(string json)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonConvert.DeserializeObject<SettingsJson>(json).install_location;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Logger.Warn($"Failed to get install_location from json {json}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
class ToolboxHistory
|
||||
{
|
||||
public List<ItemNode> history;
|
||||
|
||||
public static string GetLatestBuildFromJson(string json)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonConvert.DeserializeObject<ToolboxHistory>(json).history.LastOrDefault()?.item.build;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Logger.Warn($"Failed to get latest build from json {json}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
class ItemNode
|
||||
{
|
||||
public BuildNode item;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
class BuildNode
|
||||
{
|
||||
public string build;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ProductInfo
|
||||
{
|
||||
public string version;
|
||||
public string versionSuffix;
|
||||
|
||||
[CanBeNull]
|
||||
internal static ProductInfo GetProductInfo(string json)
|
||||
{
|
||||
try
|
||||
{
|
||||
var productInfo = JsonConvert.DeserializeObject<ProductInfo>(json);
|
||||
return productInfo;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Logger.Warn($"Failed to get version from json {json}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ReSharper disable once ClassNeverInstantiated.Global
|
||||
[Serializable]
|
||||
class ToolboxInstallData
|
||||
{
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public ActiveApplication active_application;
|
||||
|
||||
[CanBeNull]
|
||||
public static string GetLatestBuildFromJson(string json)
|
||||
{
|
||||
try
|
||||
{
|
||||
var toolbox = JsonConvert.DeserializeObject<ToolboxInstallData>(json);
|
||||
var builds = toolbox.active_application.builds;
|
||||
if (builds != null && builds.Any())
|
||||
return builds.First();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Logger.Warn($"Failed to get latest build from json {json}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
class ActiveApplication
|
||||
{
|
||||
public List<string> builds;
|
||||
}
|
||||
|
||||
#pragma warning restore 0649
|
||||
|
||||
public struct RiderInfo
|
||||
{
|
||||
// ReSharper disable once NotAccessedField.Global
|
||||
public bool IsToolbox;
|
||||
public string Presentation;
|
||||
public Version BuildNumber;
|
||||
public ProductInfo ProductInfo;
|
||||
public string Path;
|
||||
|
||||
public RiderInfo(string path, bool isToolbox)
|
||||
{
|
||||
BuildNumber = GetBuildNumber(path);
|
||||
ProductInfo = GetBuildVersion(path);
|
||||
Path = new FileInfo(path).FullName; // normalize separators
|
||||
var presentation = $"Rider {BuildNumber}";
|
||||
|
||||
if (ProductInfo != null && !string.IsNullOrEmpty(ProductInfo.version))
|
||||
{
|
||||
var suffix = string.IsNullOrEmpty(ProductInfo.versionSuffix) ? "" : $" {ProductInfo.versionSuffix}";
|
||||
presentation = $"Rider {ProductInfo.version}{suffix}";
|
||||
}
|
||||
|
||||
if (isToolbox)
|
||||
presentation += " (JetBrains Toolbox)";
|
||||
|
||||
Presentation = presentation;
|
||||
IsToolbox = isToolbox;
|
||||
}
|
||||
}
|
||||
|
||||
private static class Logger
|
||||
{
|
||||
internal static void Warn(string message, Exception e = null)
|
||||
{
|
||||
throw new Exception(message, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
124
editor/GodotTools/GodotTools/Ides/Rider/RiderPathManager.cs
Normal file
124
editor/GodotTools/GodotTools/Ides/Rider/RiderPathManager.cs
Normal file
@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Godot;
|
||||
using GodotTools.Internals;
|
||||
|
||||
namespace GodotTools.Ides.Rider
|
||||
{
|
||||
public static class RiderPathManager
|
||||
{
|
||||
public static readonly string EditorPathSettingName = "mono/editor/editor_path_optional";
|
||||
|
||||
private static string GetRiderPathFromSettings()
|
||||
{
|
||||
var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings();
|
||||
if (editorSettings.HasSetting(EditorPathSettingName))
|
||||
return (string)editorSettings.GetSetting(EditorPathSettingName);
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings();
|
||||
var editor = (ExternalEditorId)editorSettings.GetSetting("mono/editor/external_editor");
|
||||
if (editor == ExternalEditorId.Rider)
|
||||
{
|
||||
if (!editorSettings.HasSetting(EditorPathSettingName))
|
||||
{
|
||||
Globals.EditorDef(EditorPathSettingName, "Optional");
|
||||
editorSettings.AddPropertyInfo(new Godot.Collections.Dictionary
|
||||
{
|
||||
["type"] = Variant.Type.String,
|
||||
["name"] = EditorPathSettingName,
|
||||
["hint"] = PropertyHint.File,
|
||||
["hint_string"] = ""
|
||||
});
|
||||
}
|
||||
|
||||
var riderPath = (string)editorSettings.GetSetting(EditorPathSettingName);
|
||||
if (IsRiderAndExists(riderPath))
|
||||
{
|
||||
Globals.EditorDef(EditorPathSettingName, riderPath);
|
||||
return;
|
||||
}
|
||||
|
||||
var paths = RiderPathLocator.GetAllRiderPaths();
|
||||
|
||||
if (!paths.Any())
|
||||
return;
|
||||
|
||||
string newPath = paths.Last().Path;
|
||||
Globals.EditorDef(EditorPathSettingName, newPath);
|
||||
editorSettings.SetSetting(EditorPathSettingName, newPath);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsExternalEditorSetToRider(EditorSettings editorSettings)
|
||||
{
|
||||
return editorSettings.HasSetting(EditorPathSettingName) &&
|
||||
IsRider((string)editorSettings.GetSetting(EditorPathSettingName));
|
||||
}
|
||||
|
||||
public static bool IsRider(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return false;
|
||||
|
||||
if (path.IndexOfAny(System.IO.Path.GetInvalidPathChars()) != -1)
|
||||
return false;
|
||||
|
||||
var fileInfo = new FileInfo(path);
|
||||
string filename = fileInfo.Name.ToLowerInvariant();
|
||||
return filename.StartsWith("rider", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string CheckAndUpdatePath(string riderPath)
|
||||
{
|
||||
if (IsRiderAndExists(riderPath))
|
||||
{
|
||||
return riderPath;
|
||||
}
|
||||
|
||||
var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings();
|
||||
var paths = RiderPathLocator.GetAllRiderPaths();
|
||||
|
||||
if (!paths.Any())
|
||||
return null;
|
||||
|
||||
string newPath = paths.Last().Path;
|
||||
editorSettings.SetSetting(EditorPathSettingName, newPath);
|
||||
Globals.EditorDef(EditorPathSettingName, newPath);
|
||||
return newPath;
|
||||
}
|
||||
|
||||
private static bool IsRiderAndExists(string riderPath)
|
||||
{
|
||||
return !string.IsNullOrEmpty(riderPath) && IsRider(riderPath) && new FileInfo(riderPath).Exists;
|
||||
}
|
||||
|
||||
public static void OpenFile(string slnPath, string scriptPath, int line)
|
||||
{
|
||||
string pathFromSettings = GetRiderPathFromSettings();
|
||||
string path = CheckAndUpdatePath(pathFromSettings);
|
||||
|
||||
var args = new List<string>();
|
||||
args.Add(slnPath);
|
||||
if (line >= 0)
|
||||
{
|
||||
args.Add("--line");
|
||||
args.Add((line + 1).ToString()); // https://github.com/JetBrains/godot-support/issues/61
|
||||
}
|
||||
args.Add(scriptPath);
|
||||
try
|
||||
{
|
||||
Utils.OS.RunProcess(path, args);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
GD.PushError($"Error when trying to run code editor: JetBrains Rider. Exception message: '{e.Message}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
50
editor/GodotTools/GodotTools/Internals/EditorProgress.cs
Normal file
50
editor/GodotTools/GodotTools/Internals/EditorProgress.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Godot;
|
||||
|
||||
namespace GodotTools.Internals
|
||||
{
|
||||
public class EditorProgress : IDisposable
|
||||
{
|
||||
public string Task { get; }
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern void internal_Create(string task, string label, int amount, bool canCancel);
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern void internal_Dispose(string task);
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern bool internal_Step(string task, string state, int step, bool forceRefresh);
|
||||
|
||||
public EditorProgress(string task, string label, int amount, bool canCancel = false)
|
||||
{
|
||||
Task = task;
|
||||
internal_Create(task, label, amount, canCancel);
|
||||
}
|
||||
|
||||
~EditorProgress()
|
||||
{
|
||||
// Should never rely on the GC to dispose EditorProgress.
|
||||
// It should be disposed immediately when the task finishes.
|
||||
GD.PushError("EditorProgress disposed by the Garbage Collector");
|
||||
Dispose();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
internal_Dispose(Task);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public void Step(string state, int step = -1, bool forceRefresh = true)
|
||||
{
|
||||
internal_Step(Task, state, step, forceRefresh);
|
||||
}
|
||||
|
||||
public bool TryStep(string state, int step = -1, bool forceRefresh = true)
|
||||
{
|
||||
return internal_Step(Task, state, step, forceRefresh);
|
||||
}
|
||||
}
|
||||
}
|
39
editor/GodotTools/GodotTools/Internals/Globals.cs
Normal file
39
editor/GodotTools/GodotTools/Internals/Globals.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace GodotTools.Internals
|
||||
{
|
||||
public static class Globals
|
||||
{
|
||||
public static float EditorScale => internal_EditorScale();
|
||||
|
||||
public static object GlobalDef(string setting, object defaultValue, bool restartIfChanged = false) =>
|
||||
internal_GlobalDef(setting, defaultValue, restartIfChanged);
|
||||
|
||||
public static object EditorDef(string setting, object defaultValue, bool restartIfChanged = false) =>
|
||||
internal_EditorDef(setting, defaultValue, restartIfChanged);
|
||||
|
||||
public static object EditorShortcut(string setting) =>
|
||||
internal_EditorShortcut(setting);
|
||||
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
public static string TTR(this string text) => internal_TTR(text);
|
||||
|
||||
// Internal Calls
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern float internal_EditorScale();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern object internal_GlobalDef(string setting, object defaultValue, bool restartIfChanged);
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern object internal_EditorDef(string setting, object defaultValue, bool restartIfChanged);
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern object internal_EditorShortcut(string setting);
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern string internal_TTR(string text);
|
||||
}
|
||||
}
|
107
editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs
Normal file
107
editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs
Normal file
@ -0,0 +1,107 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace GodotTools.Internals
|
||||
{
|
||||
public static class GodotSharpDirs
|
||||
{
|
||||
public static string ResDataDir => internal_ResDataDir();
|
||||
public static string ResMetadataDir => internal_ResMetadataDir();
|
||||
public static string ResAssembliesBaseDir => internal_ResAssembliesBaseDir();
|
||||
public static string ResAssembliesDir => internal_ResAssembliesDir();
|
||||
public static string ResConfigDir => internal_ResConfigDir();
|
||||
public static string ResTempDir => internal_ResTempDir();
|
||||
public static string ResTempAssembliesBaseDir => internal_ResTempAssembliesBaseDir();
|
||||
public static string ResTempAssembliesDir => internal_ResTempAssembliesDir();
|
||||
|
||||
public static string MonoUserDir => internal_MonoUserDir();
|
||||
public static string MonoLogsDir => internal_MonoLogsDir();
|
||||
|
||||
#region Tools-only
|
||||
public static string MonoSolutionsDir => internal_MonoSolutionsDir();
|
||||
public static string BuildLogsDirs => internal_BuildLogsDirs();
|
||||
|
||||
public static string ProjectAssemblyName => internal_ProjectAssemblyName();
|
||||
public static string ProjectSlnPath => internal_ProjectSlnPath();
|
||||
public static string ProjectCsProjPath => internal_ProjectCsProjPath();
|
||||
|
||||
public static string DataEditorToolsDir => internal_DataEditorToolsDir();
|
||||
public static string DataEditorPrebuiltApiDir => internal_DataEditorPrebuiltApiDir();
|
||||
#endregion
|
||||
|
||||
public static string DataMonoEtcDir => internal_DataMonoEtcDir();
|
||||
public static string DataMonoLibDir => internal_DataMonoLibDir();
|
||||
|
||||
#region Windows-only
|
||||
public static string DataMonoBinDir => internal_DataMonoBinDir();
|
||||
#endregion
|
||||
|
||||
|
||||
#region Internal
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern string internal_ResDataDir();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern string internal_ResMetadataDir();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern string internal_ResAssembliesBaseDir();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern string internal_ResAssembliesDir();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern string internal_ResConfigDir();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern string internal_ResTempDir();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern string internal_ResTempAssembliesBaseDir();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern string internal_ResTempAssembliesDir();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern string internal_MonoUserDir();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern string internal_MonoLogsDir();
|
||||
|
||||
#region Tools-only
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern string internal_MonoSolutionsDir();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern string internal_BuildLogsDirs();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern string internal_ProjectAssemblyName();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern string internal_ProjectSlnPath();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern string internal_ProjectCsProjPath();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern string internal_DataEditorToolsDir();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern string internal_DataEditorPrebuiltApiDir();
|
||||
#endregion
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern string internal_DataMonoEtcDir();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern string internal_DataMonoLibDir();
|
||||
|
||||
#region Windows-only
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern string internal_DataMonoBinDir();
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
123
editor/GodotTools/GodotTools/Internals/Internal.cs
Normal file
123
editor/GodotTools/GodotTools/Internals/Internal.cs
Normal file
@ -0,0 +1,123 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
using GodotTools.IdeMessaging.Requests;
|
||||
|
||||
namespace GodotTools.Internals
|
||||
{
|
||||
public static class Internal
|
||||
{
|
||||
public const string CSharpLanguageType = "CSharpScript";
|
||||
public const string CSharpLanguageExtension = ".cs";
|
||||
|
||||
public static string UpdateApiAssembliesFromPrebuilt(string config) =>
|
||||
internal_UpdateApiAssembliesFromPrebuilt(config);
|
||||
|
||||
public static string FullTemplatesDir =>
|
||||
internal_FullTemplatesDir();
|
||||
|
||||
public static string SimplifyGodotPath(this string path) => internal_SimplifyGodotPath(path);
|
||||
|
||||
public static bool IsOsxAppBundleInstalled(string bundleId) => internal_IsOsxAppBundleInstalled(bundleId);
|
||||
|
||||
public static bool GodotIs32Bits() => internal_GodotIs32Bits();
|
||||
|
||||
public static bool GodotIsRealTDouble() => internal_GodotIsRealTDouble();
|
||||
|
||||
public static void GodotMainIteration() => internal_GodotMainIteration();
|
||||
|
||||
public static ulong GetCoreApiHash() => internal_GetCoreApiHash();
|
||||
|
||||
public static ulong GetEditorApiHash() => internal_GetEditorApiHash();
|
||||
|
||||
public static bool IsAssembliesReloadingNeeded() => internal_IsAssembliesReloadingNeeded();
|
||||
|
||||
public static void ReloadAssemblies(bool softReload) => internal_ReloadAssemblies(softReload);
|
||||
|
||||
public static void ScriptEditorDebuggerReloadScripts() => internal_ScriptEditorDebuggerReloadScripts();
|
||||
|
||||
public static bool ScriptEditorEdit(Resource resource, int line, int col, bool grabFocus = true) =>
|
||||
internal_ScriptEditorEdit(resource, line, col, grabFocus);
|
||||
|
||||
public static void EditorNodeShowScriptScreen() => internal_EditorNodeShowScriptScreen();
|
||||
|
||||
public static Dictionary<string, object> GetScriptsMetadataOrNothing() =>
|
||||
internal_GetScriptsMetadataOrNothing(typeof(Dictionary<string, object>));
|
||||
|
||||
public static string MonoWindowsInstallRoot => internal_MonoWindowsInstallRoot();
|
||||
|
||||
public static void EditorRunPlay() => internal_EditorRunPlay();
|
||||
|
||||
public static void EditorRunStop() => internal_EditorRunStop();
|
||||
|
||||
public static void ScriptEditorDebugger_ReloadScripts() => internal_ScriptEditorDebugger_ReloadScripts();
|
||||
|
||||
public static string[] CodeCompletionRequest(CodeCompletionRequest.CompletionKind kind, string scriptFile) =>
|
||||
internal_CodeCompletionRequest((int)kind, scriptFile);
|
||||
|
||||
#region Internal
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern string internal_UpdateApiAssembliesFromPrebuilt(string config);
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern string internal_FullTemplatesDir();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern string internal_SimplifyGodotPath(this string path);
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern bool internal_IsOsxAppBundleInstalled(string bundleId);
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern bool internal_GodotIs32Bits();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern bool internal_GodotIsRealTDouble();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern void internal_GodotMainIteration();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern ulong internal_GetCoreApiHash();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern ulong internal_GetEditorApiHash();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern bool internal_IsAssembliesReloadingNeeded();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern void internal_ReloadAssemblies(bool softReload);
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern void internal_ScriptEditorDebuggerReloadScripts();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern bool internal_ScriptEditorEdit(Resource resource, int line, int col, bool grabFocus);
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern void internal_EditorNodeShowScriptScreen();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern Dictionary<string, object> internal_GetScriptsMetadataOrNothing(Type dictType);
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern string internal_MonoWindowsInstallRoot();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern void internal_EditorRunPlay();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern void internal_EditorRunStop();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern void internal_ScriptEditorDebugger_ReloadScripts();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern string[] internal_CodeCompletionRequest(int kind, string scriptFile);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
61
editor/GodotTools/GodotTools/Internals/ScriptClassParser.cs
Normal file
61
editor/GodotTools/GodotTools/Internals/ScriptClassParser.cs
Normal file
@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
|
||||
namespace GodotTools.Internals
|
||||
{
|
||||
public static class ScriptClassParser
|
||||
{
|
||||
public class ClassDecl
|
||||
{
|
||||
public string Name { get; }
|
||||
public string Namespace { get; }
|
||||
public bool Nested { get; }
|
||||
public int BaseCount { get; }
|
||||
|
||||
public string SearchName => Nested ?
|
||||
Name.Substring(Name.LastIndexOf(".", StringComparison.Ordinal) + 1) :
|
||||
Name;
|
||||
|
||||
public ClassDecl(string name, string @namespace, bool nested, int baseCount)
|
||||
{
|
||||
Name = name;
|
||||
Namespace = @namespace;
|
||||
Nested = nested;
|
||||
BaseCount = baseCount;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern Error internal_ParseFile(string filePath, Array<Dictionary> classes, out string errorStr);
|
||||
|
||||
public static Error ParseFile(string filePath, out IEnumerable<ClassDecl> classes, out string errorStr)
|
||||
{
|
||||
var classesArray = new Array<Dictionary>();
|
||||
var error = internal_ParseFile(filePath, classesArray, out errorStr);
|
||||
if (error != Error.Ok)
|
||||
{
|
||||
classes = null;
|
||||
return error;
|
||||
}
|
||||
|
||||
var classesList = new List<ClassDecl>();
|
||||
|
||||
foreach (var classDeclDict in classesArray)
|
||||
{
|
||||
classesList.Add(new ClassDecl(
|
||||
(string)classDeclDict["name"],
|
||||
(string)classDeclDict["namespace"],
|
||||
(bool)classDeclDict["nested"],
|
||||
(int)classDeclDict["base_count"]
|
||||
));
|
||||
}
|
||||
|
||||
classes = classesList;
|
||||
|
||||
return Error.Ok;
|
||||
}
|
||||
}
|
||||
}
|
19
editor/GodotTools/GodotTools/PlaySettings.cs
Normal file
19
editor/GodotTools/GodotTools/PlaySettings.cs
Normal file
@ -0,0 +1,19 @@
|
||||
namespace GodotTools
|
||||
{
|
||||
public struct PlaySettings
|
||||
{
|
||||
public bool HasDebugger { get; }
|
||||
public string DebuggerHost { get; }
|
||||
public int DebuggerPort { get; }
|
||||
|
||||
public bool BuildBeforePlaying { get; }
|
||||
|
||||
public PlaySettings(string debuggerHost, int debuggerPort, bool buildBeforePlaying)
|
||||
{
|
||||
HasDebugger = true;
|
||||
DebuggerHost = debuggerHost;
|
||||
DebuggerPort = debuggerPort;
|
||||
BuildBeforePlaying = buildBeforePlaying;
|
||||
}
|
||||
}
|
||||
}
|
29
editor/GodotTools/GodotTools/Utils/CollectionExtensions.cs
Normal file
29
editor/GodotTools/GodotTools/Utils/CollectionExtensions.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace GodotTools.Utils
|
||||
{
|
||||
public static class CollectionExtensions
|
||||
{
|
||||
public static T SelectFirstNotNull<T>(this IEnumerable<T> enumerable, Func<T, T> predicate, T orElse = null)
|
||||
where T : class
|
||||
{
|
||||
foreach (T elem in enumerable)
|
||||
{
|
||||
T result = predicate(elem);
|
||||
if (result != null)
|
||||
return result;
|
||||
}
|
||||
|
||||
return orElse;
|
||||
}
|
||||
|
||||
public static IEnumerable<string> EnumerateLines(this TextReader textReader)
|
||||
{
|
||||
string line;
|
||||
while ((line = textReader.ReadLine()) != null)
|
||||
yield return line;
|
||||
}
|
||||
}
|
||||
}
|
40
editor/GodotTools/GodotTools/Utils/Directory.cs
Normal file
40
editor/GodotTools/GodotTools/Utils/Directory.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using System.IO;
|
||||
using Godot;
|
||||
|
||||
namespace GodotTools.Utils
|
||||
{
|
||||
public static class Directory
|
||||
{
|
||||
private static string GlobalizePath(this string path)
|
||||
{
|
||||
return ProjectSettings.GlobalizePath(path);
|
||||
}
|
||||
|
||||
public static bool Exists(string path)
|
||||
{
|
||||
return System.IO.Directory.Exists(path.GlobalizePath());
|
||||
}
|
||||
|
||||
/// Create directory recursively
|
||||
public static DirectoryInfo CreateDirectory(string path)
|
||||
{
|
||||
return System.IO.Directory.CreateDirectory(path.GlobalizePath());
|
||||
}
|
||||
|
||||
public static void Delete(string path, bool recursive)
|
||||
{
|
||||
System.IO.Directory.Delete(path.GlobalizePath(), recursive);
|
||||
}
|
||||
|
||||
|
||||
public static string[] GetDirectories(string path, string searchPattern, SearchOption searchOption)
|
||||
{
|
||||
return System.IO.Directory.GetDirectories(path.GlobalizePath(), searchPattern, searchOption);
|
||||
}
|
||||
|
||||
public static string[] GetFiles(string path, string searchPattern, SearchOption searchOption)
|
||||
{
|
||||
return System.IO.Directory.GetFiles(path.GlobalizePath(), searchPattern, searchOption);
|
||||
}
|
||||
}
|
||||
}
|
43
editor/GodotTools/GodotTools/Utils/File.cs
Normal file
43
editor/GodotTools/GodotTools/Utils/File.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using Godot;
|
||||
|
||||
namespace GodotTools.Utils
|
||||
{
|
||||
public static class File
|
||||
{
|
||||
private static string GlobalizePath(this string path)
|
||||
{
|
||||
return ProjectSettings.GlobalizePath(path);
|
||||
}
|
||||
|
||||
public static void WriteAllText(string path, string contents)
|
||||
{
|
||||
System.IO.File.WriteAllText(path.GlobalizePath(), contents);
|
||||
}
|
||||
|
||||
public static bool Exists(string path)
|
||||
{
|
||||
return System.IO.File.Exists(path.GlobalizePath());
|
||||
}
|
||||
|
||||
public static DateTime GetLastWriteTime(string path)
|
||||
{
|
||||
return System.IO.File.GetLastWriteTime(path.GlobalizePath());
|
||||
}
|
||||
|
||||
public static void Delete(string path)
|
||||
{
|
||||
System.IO.File.Delete(path.GlobalizePath());
|
||||
}
|
||||
|
||||
public static void Copy(string sourceFileName, string destFileName)
|
||||
{
|
||||
System.IO.File.Copy(sourceFileName.GlobalizePath(), destFileName.GlobalizePath(), overwrite: true);
|
||||
}
|
||||
|
||||
public static byte[] ReadAllBytes(string path)
|
||||
{
|
||||
return System.IO.File.ReadAllBytes(path.GlobalizePath());
|
||||
}
|
||||
}
|
||||
}
|
48
editor/GodotTools/GodotTools/Utils/FsPathUtils.cs
Normal file
48
editor/GodotTools/GodotTools/Utils/FsPathUtils.cs
Normal file
@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using Godot;
|
||||
using GodotTools.Core;
|
||||
using JetBrains.Annotations;
|
||||
using Path = System.IO.Path;
|
||||
|
||||
namespace GodotTools.Utils
|
||||
{
|
||||
public static class FsPathUtils
|
||||
{
|
||||
private static readonly string _resourcePath = ProjectSettings.GlobalizePath("res://");
|
||||
|
||||
private static bool PathStartsWithAlreadyNorm(this string childPath, string parentPath)
|
||||
{
|
||||
// This won't work for Linux/macOS case insensitive file systems, but it's enough for our current problems
|
||||
bool caseSensitive = !OS.IsWindows;
|
||||
|
||||
string parentPathNorm = parentPath.NormalizePath() + Path.DirectorySeparatorChar;
|
||||
string childPathNorm = childPath.NormalizePath() + Path.DirectorySeparatorChar;
|
||||
|
||||
return childPathNorm.StartsWith(parentPathNorm,
|
||||
caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static bool PathStartsWith(this string childPath, string parentPath)
|
||||
{
|
||||
string childPathNorm = childPath.NormalizePath() + Path.DirectorySeparatorChar;
|
||||
string parentPathNorm = parentPath.NormalizePath() + Path.DirectorySeparatorChar;
|
||||
|
||||
return childPathNorm.PathStartsWithAlreadyNorm(parentPathNorm);
|
||||
}
|
||||
|
||||
[CanBeNull]
|
||||
public static string LocalizePathWithCaseChecked(string path)
|
||||
{
|
||||
string pathNorm = path.NormalizePath() + Path.DirectorySeparatorChar;
|
||||
string resourcePathNorm = _resourcePath.NormalizePath() + Path.DirectorySeparatorChar;
|
||||
|
||||
if (!pathNorm.PathStartsWithAlreadyNorm(resourcePathNorm))
|
||||
return null;
|
||||
|
||||
string result = "res://" + pathNorm.Substring(resourcePathNorm.Length);
|
||||
|
||||
// Remove the last separator we added
|
||||
return result.Substring(0, result.Length - 1);
|
||||
}
|
||||
}
|
||||
}
|
221
editor/GodotTools/GodotTools/Utils/OS.cs
Normal file
221
editor/GodotTools/GodotTools/Utils/OS.cs
Normal file
@ -0,0 +1,221 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace GodotTools.Utils
|
||||
{
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
public static class OS
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern string GetPlatformName();
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall)]
|
||||
private static extern bool UnixFileHasExecutableAccess(string filePath);
|
||||
|
||||
public static class Names
|
||||
{
|
||||
public const string Windows = "Windows";
|
||||
public const string OSX = "OSX";
|
||||
public const string X11 = "X11";
|
||||
public const string Server = "Server";
|
||||
public const string UWP = "UWP";
|
||||
public const string Haiku = "Haiku";
|
||||
public const string Android = "Android";
|
||||
public const string iOS = "iOS";
|
||||
public const string HTML5 = "HTML5";
|
||||
}
|
||||
|
||||
public static class Platforms
|
||||
{
|
||||
public const string Windows = "windows";
|
||||
public const string OSX = "osx";
|
||||
public const string X11 = "x11";
|
||||
public const string Server = "server";
|
||||
public const string UWP = "uwp";
|
||||
public const string Haiku = "haiku";
|
||||
public const string Android = "android";
|
||||
public const string iOS = "iphone";
|
||||
public const string HTML5 = "javascript";
|
||||
}
|
||||
|
||||
public static readonly Dictionary<string, string> PlatformNameMap = new Dictionary<string, string>
|
||||
{
|
||||
[Names.Windows] = Platforms.Windows,
|
||||
[Names.OSX] = Platforms.OSX,
|
||||
[Names.X11] = Platforms.X11,
|
||||
[Names.Server] = Platforms.Server,
|
||||
[Names.UWP] = Platforms.UWP,
|
||||
[Names.Haiku] = Platforms.Haiku,
|
||||
[Names.Android] = Platforms.Android,
|
||||
[Names.iOS] = Platforms.iOS,
|
||||
[Names.HTML5] = Platforms.HTML5
|
||||
};
|
||||
|
||||
private static bool IsOS(string name)
|
||||
{
|
||||
return name.Equals(GetPlatformName(), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool IsAnyOS(IEnumerable<string> names)
|
||||
{
|
||||
return names.Any(p => p.Equals(GetPlatformName(), StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static readonly Lazy<bool> _isWindows = new Lazy<bool>(() => IsOS(Names.Windows));
|
||||
private static readonly Lazy<bool> _isOSX = new Lazy<bool>(() => IsOS(Names.OSX));
|
||||
private static readonly Lazy<bool> _isX11 = new Lazy<bool>(() => IsOS(Names.X11));
|
||||
private static readonly Lazy<bool> _isServer = new Lazy<bool>(() => IsOS(Names.Server));
|
||||
private static readonly Lazy<bool> _isUWP = new Lazy<bool>(() => IsOS(Names.UWP));
|
||||
private static readonly Lazy<bool> _isHaiku = new Lazy<bool>(() => IsOS(Names.Haiku));
|
||||
private static readonly Lazy<bool> _isAndroid = new Lazy<bool>(() => IsOS(Names.Android));
|
||||
private static readonly Lazy<bool> _isiOS = new Lazy<bool>(() => IsOS(Names.iOS));
|
||||
private static readonly Lazy<bool> _isHTML5 = new Lazy<bool>(() => IsOS(Names.HTML5));
|
||||
private static readonly Lazy<bool> _isUnixLike = new Lazy<bool>(() => IsAnyOS(UnixLikePlatforms));
|
||||
|
||||
public static bool IsWindows => _isWindows.Value || IsUWP;
|
||||
public static bool IsOSX => _isOSX.Value;
|
||||
public static bool IsX11 => _isX11.Value;
|
||||
public static bool IsServer => _isServer.Value;
|
||||
public static bool IsUWP => _isUWP.Value;
|
||||
public static bool IsHaiku => _isHaiku.Value;
|
||||
public static bool IsAndroid => _isAndroid.Value;
|
||||
public static bool IsiOS => _isiOS.Value;
|
||||
public static bool IsHTML5 => _isHTML5.Value;
|
||||
|
||||
private static readonly string[] UnixLikePlatforms = {Names.OSX, Names.X11, Names.Server, Names.Haiku, Names.Android, Names.iOS};
|
||||
|
||||
public static bool IsUnixLike => _isUnixLike.Value;
|
||||
|
||||
public static char PathSep => IsWindows ? ';' : ':';
|
||||
|
||||
public static string PathWhich([NotNull] string name)
|
||||
{
|
||||
if (IsWindows)
|
||||
return PathWhichWindows(name);
|
||||
|
||||
return PathWhichUnix(name);
|
||||
}
|
||||
|
||||
private static string PathWhichWindows([NotNull] string name)
|
||||
{
|
||||
string[] windowsExts = Environment.GetEnvironmentVariable("PATHEXT")?.Split(PathSep) ?? Array.Empty<string>();
|
||||
string[] pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(PathSep);
|
||||
char[] invalidPathChars = Path.GetInvalidPathChars();
|
||||
|
||||
var searchDirs = new List<string>();
|
||||
|
||||
if (pathDirs != null)
|
||||
{
|
||||
foreach (var pathDir in pathDirs)
|
||||
{
|
||||
if (pathDir.IndexOfAny(invalidPathChars) != -1)
|
||||
continue;
|
||||
|
||||
searchDirs.Add(pathDir);
|
||||
}
|
||||
}
|
||||
|
||||
string nameExt = Path.GetExtension(name);
|
||||
bool hasPathExt = !string.IsNullOrEmpty(nameExt) &&
|
||||
windowsExts.Contains(nameExt, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
searchDirs.Add(System.IO.Directory.GetCurrentDirectory()); // last in the list
|
||||
|
||||
if (hasPathExt)
|
||||
return searchDirs.Select(dir => Path.Combine(dir, name)).FirstOrDefault(File.Exists);
|
||||
|
||||
return (from dir in searchDirs
|
||||
select Path.Combine(dir, name)
|
||||
into path
|
||||
from ext in windowsExts
|
||||
select path + ext).FirstOrDefault(File.Exists);
|
||||
}
|
||||
|
||||
private static string PathWhichUnix([NotNull] string name)
|
||||
{
|
||||
string[] pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(PathSep);
|
||||
char[] invalidPathChars = Path.GetInvalidPathChars();
|
||||
|
||||
var searchDirs = new List<string>();
|
||||
|
||||
if (pathDirs != null)
|
||||
{
|
||||
foreach (var pathDir in pathDirs)
|
||||
{
|
||||
if (pathDir.IndexOfAny(invalidPathChars) != -1)
|
||||
continue;
|
||||
|
||||
searchDirs.Add(pathDir);
|
||||
}
|
||||
}
|
||||
|
||||
searchDirs.Add(System.IO.Directory.GetCurrentDirectory()); // last in the list
|
||||
|
||||
return searchDirs.Select(dir => Path.Combine(dir, name))
|
||||
.FirstOrDefault(path => File.Exists(path) && UnixFileHasExecutableAccess(path));
|
||||
}
|
||||
|
||||
public static void RunProcess(string command, IEnumerable<string> arguments)
|
||||
{
|
||||
// TODO: Once we move to .NET Standard 2.1 we can use ProcessStartInfo.ArgumentList instead
|
||||
string CmdLineArgsToString(IEnumerable<string> args)
|
||||
{
|
||||
// Not perfect, but as long as we are careful...
|
||||
return string.Join(" ", args.Select(arg => arg.Contains(" ") ? $@"""{arg}""" : arg));
|
||||
}
|
||||
|
||||
var startInfo = new ProcessStartInfo(command, CmdLineArgsToString(arguments))
|
||||
{
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
using (Process process = Process.Start(startInfo))
|
||||
{
|
||||
if (process == null)
|
||||
throw new Exception("No process was started");
|
||||
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
if (IsWindows && process.Id > 0)
|
||||
User32Dll.AllowSetForegroundWindow(process.Id); // allows application to focus itself
|
||||
}
|
||||
}
|
||||
|
||||
public static int ExecuteCommand(string command, IEnumerable<string> arguments)
|
||||
{
|
||||
// TODO: Once we move to .NET Standard 2.1 we can use ProcessStartInfo.ArgumentList instead
|
||||
string CmdLineArgsToString(IEnumerable<string> args)
|
||||
{
|
||||
// Not perfect, but as long as we are careful...
|
||||
return string.Join(" ", args.Select(arg => arg.Contains(" ") ? $@"""{arg}""" : arg));
|
||||
}
|
||||
|
||||
var startInfo = new ProcessStartInfo(command, CmdLineArgsToString(arguments));
|
||||
|
||||
Console.WriteLine($"Executing: \"{startInfo.FileName}\" {startInfo.Arguments}");
|
||||
|
||||
// Print the output
|
||||
startInfo.RedirectStandardOutput = false;
|
||||
startInfo.RedirectStandardError = false;
|
||||
|
||||
startInfo.UseShellExecute = false;
|
||||
|
||||
using (var process = new Process { StartInfo = startInfo })
|
||||
{
|
||||
process.Start();
|
||||
process.WaitForExit();
|
||||
|
||||
return process.ExitCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user