diff --git a/.github/workflows/windows_builds.yml b/.github/workflows/windows_builds.yml index 2794c83e221..18ed92b57f0 100644 --- a/.github/workflows/windows_builds.yml +++ b/.github/workflows/windows_builds.yml @@ -28,7 +28,7 @@ jobs: target: editor tests: true # Skip debug symbols, they're way too big with MSVC. - sconsflags: debug_symbols=no vsproj=yes windows_subsystem=console + sconsflags: debug_symbols=no vsproj=yes vsproj_gen_only=no windows_subsystem=console bin: "./bin/godot.windows.editor.x86_64.exe" - name: Template (target=template_release) diff --git a/.gitignore b/.gitignore index 3c6f279a9c4..b415eede94b 100644 --- a/.gitignore +++ b/.gitignore @@ -367,3 +367,4 @@ $RECYCLE.BIN/ *.msm *.msp *.lnk +*.generated.props diff --git a/SConstruct b/SConstruct index 6a4dea2c092..f0f53ddc65a 100644 --- a/SConstruct +++ b/SConstruct @@ -1000,9 +1000,6 @@ if selected_platform in platform_list: # Microsoft Visual Studio Project Generation if env["vsproj"]: - if os.name != "nt": - print("Error: The `vsproj` option is only usable on Windows with Visual Studio.") - Exit(255) env["CPPPATH"] = [Dir(path) for path in env["CPPPATH"]] methods.generate_vs_project(env, ARGUMENTS, env["vsproj_name"]) methods.generate_cpp_hint_file("cpp.hint") diff --git a/methods.py b/methods.py index f36591d211c..c22b1f11e46 100644 --- a/methods.py +++ b/methods.py @@ -774,161 +774,6 @@ def add_to_vs_project(env, sources): env.vs_srcs += [basename + ".cpp"] -def generate_vs_project(env, original_args, project_name="godot"): - batch_file = find_visual_c_batch_file(env) - filtered_args = original_args.copy() - # Ignore the "vsproj" option to not regenerate the VS project on every build - filtered_args.pop("vsproj", None) - # The "platform" option is ignored because only the Windows platform is currently supported for VS projects - filtered_args.pop("platform", None) - # The "target" option is ignored due to the way how targets configuration is performed for VS projects (there is a separate project configuration for each target) - filtered_args.pop("target", None) - # The "progress" option is ignored as the current compilation progress indication doesn't work in VS - filtered_args.pop("progress", None) - - if batch_file: - - class ModuleConfigs(Mapping): - # This version information (Win32, x64, Debug, Release) seems to be - # required for Visual Studio to understand that it needs to generate an NMAKE - # project. Do not modify without knowing what you are doing. - PLATFORMS = ["Win32", "x64"] - PLATFORM_IDS = ["x86_32", "x86_64"] - CONFIGURATIONS = ["editor", "template_release", "template_debug"] - DEV_SUFFIX = ".dev" if env["dev_build"] else "" - - @staticmethod - def for_every_variant(value): - return [value for _ in range(len(ModuleConfigs.CONFIGURATIONS) * len(ModuleConfigs.PLATFORMS))] - - def __init__(self): - shared_targets_array = [] - self.names = [] - self.arg_dict = { - "variant": [], - "runfile": shared_targets_array, - "buildtarget": shared_targets_array, - "cpppaths": [], - "cppdefines": [], - "cmdargs": [], - } - self.add_mode() # default - - def add_mode( - self, - name: str = "", - includes: str = "", - cli_args: str = "", - defines=None, - ): - if defines is None: - defines = [] - self.names.append(name) - self.arg_dict["variant"] += [ - f'{config}{f"_[{name}]" if name else ""}|{platform}' - for config in ModuleConfigs.CONFIGURATIONS - for platform in ModuleConfigs.PLATFORMS - ] - self.arg_dict["runfile"] += [ - f'bin\\godot.windows.{config}{ModuleConfigs.DEV_SUFFIX}{".double" if env["precision"] == "double" else ""}.{plat_id}{f".{name}" if name else ""}.exe' - for config in ModuleConfigs.CONFIGURATIONS - for plat_id in ModuleConfigs.PLATFORM_IDS - ] - self.arg_dict["cpppaths"] += ModuleConfigs.for_every_variant(env["CPPPATH"] + [includes]) - self.arg_dict["cppdefines"] += ModuleConfigs.for_every_variant(list(env["CPPDEFINES"]) + defines) - self.arg_dict["cmdargs"] += ModuleConfigs.for_every_variant(cli_args) - - def build_commandline(self, commands): - configuration_getter = ( - "$(Configuration" - + "".join([f'.Replace("{name}", "")' for name in self.names[1:]]) - + '.Replace("_[]", "")' - + ")" - ) - - common_build_prefix = [ - 'cmd /V /C set "plat=$(PlatformTarget)"', - '(if "$(PlatformTarget)"=="x64" (set "plat=x86_amd64"))', - 'call "' + batch_file + '" !plat!', - ] - - # Windows allows us to have spaces in paths, so we need - # to double quote off the directory. However, the path ends - # in a backslash, so we need to remove this, lest it escape the - # last double quote off, confusing MSBuild - common_build_postfix = [ - "--directory=\"$(ProjectDir.TrimEnd('\\'))\"", - "platform=windows", - f"target={configuration_getter}", - "progress=no", - ] - - for arg, value in filtered_args.items(): - common_build_postfix.append(f"{arg}={value}") - - result = " ^& ".join(common_build_prefix + [" ".join([commands] + common_build_postfix)]) - return result - - # Mappings interface definitions - - def __iter__(self) -> Iterator[str]: - for x in self.arg_dict: - yield x - - def __len__(self) -> int: - return len(self.names) - - def __getitem__(self, k: str): - return self.arg_dict[k] - - add_to_vs_project(env, env.core_sources) - add_to_vs_project(env, env.drivers_sources) - add_to_vs_project(env, env.main_sources) - add_to_vs_project(env, env.modules_sources) - add_to_vs_project(env, env.scene_sources) - add_to_vs_project(env, env.servers_sources) - if env["tests"]: - add_to_vs_project(env, env.tests_sources) - if env.editor_build: - add_to_vs_project(env, env.editor_sources) - - for header in glob_recursive("**/*.h"): - env.vs_incs.append(str(header)) - - module_configs = ModuleConfigs() - - if env.get("module_mono_enabled"): - mono_defines = [("GD_MONO_HOT_RELOAD",)] if env.editor_build else [] - module_configs.add_mode( - "mono", - cli_args="module_mono_enabled=yes", - defines=mono_defines, - ) - - scons_cmd = "scons" - - path_to_venv = os.getenv("VIRTUAL_ENV") - path_to_scons_exe = Path(str(path_to_venv)) / "Scripts" / "scons.exe" - if path_to_venv and path_to_scons_exe.exists(): - scons_cmd = str(path_to_scons_exe) - - env["MSVSBUILDCOM"] = module_configs.build_commandline(scons_cmd) - env["MSVSREBUILDCOM"] = module_configs.build_commandline(f"{scons_cmd} vsproj=yes") - env["MSVSCLEANCOM"] = module_configs.build_commandline(f"{scons_cmd} --clean") - if not env.get("MSVS"): - env["MSVS"]["PROJECTSUFFIX"] = ".vcxproj" - env["MSVS"]["SOLUTIONSUFFIX"] = ".sln" - env.MSVSProject( - target=["#" + project_name + env["MSVSPROJECTSUFFIX"]], - incs=env.vs_incs, - srcs=env.vs_srcs, - auto_build_solution=1, - **module_configs, - ) - else: - print("Could not locate Visual Studio batch file to set up the build environment. Not generating VS project.") - - def precious_program(env, program, sources, **args): program = env.ProgramOriginal(program, sources, **args) env.Precious(program) @@ -1229,3 +1074,456 @@ def dump(env): with open(".scons_env.json", "w") as f: dump(env.Dictionary(), f, indent=4, default=non_serializable) + + +# Custom Visual Studio project generation logic that supports any platform that has a msvs.py +# script, so Visual Studio can be used to run scons for any platform, with the right defines per target. +# Invoked with scons vsproj=yes +# +# Only platforms that opt in to vs proj generation by having a msvs.py file in the platform folder are included. +# Platforms with a msvs.py file will be added to the solution, but only the current active platform+target+arch +# will have a build configuration generated, because we only know what the right defines/includes/flags/etc are +# on the active build target. +# +# Platforms that don't support an editor target will have a dummy editor target that won't do anything on build, +# but will have the files and configuration for the windows editor target. +# +# To generate build configuration files for all platforms+targets+arch combinations, users can call +# scons vsproj=yes +# for each combination of platform+target+arch. This will generate the relevant vs project files but +# skip the build process. This lets project files be quickly generated even if there are build errors. +# +# To generate AND build from the command line: +# scons vsproj=yes vsproj_gen_only=yes +def generate_vs_project(env, original_args, project_name="godot"): + # Augmented glob_recursive that also fills the dirs argument with traversed directories that have content. + def glob_recursive_2(pattern, dirs, node="."): + from SCons import Node + from SCons.Script import Glob + + results = [] + for f in Glob(str(node) + "/*", source=True): + if type(f) is Node.FS.Dir: + results += glob_recursive_2(pattern, dirs, f) + r = Glob(str(node) + "/" + pattern, source=True) + if len(r) > 0 and not str(node) in dirs: + d = "" + for part in str(node).split("\\"): + d += part + if not d in dirs: + dirs.append(d) + d += "\\" + results += r + return results + + def get_bool(args, option, default): + from SCons.Variables.BoolVariable import _text2bool + + val = args.get(option, default) + if val is not None: + try: + return _text2bool(val) + except: + return default + else: + return default + + def format_key_value(v): + if type(v) in [tuple, list]: + return v[0] if len(v) == 1 else f"{v[0]}={v[1]}" + return v + + filtered_args = original_args.copy() + + # Ignore the "vsproj" option to not regenerate the VS project on every build + filtered_args.pop("vsproj", None) + + # This flag allows users to regenerate the proj files but skip the building process. + # This lets projects be regenerated even if there are build errors. + filtered_args.pop("vsproj_gen_only", None) + + # The "progress" option is ignored as the current compilation progress indication doesn't work in VS + filtered_args.pop("progress", None) + + # We add these three manually because they might not be explicitly passed in, and it's important to always set them. + filtered_args.pop("platform", None) + filtered_args.pop("target", None) + filtered_args.pop("arch", None) + + platform = env["platform"] + target = env["target"] + arch = env["arch"] + + vs_configuration = {} + common_build_prefix = [] + confs = [] + for x in sorted(glob.glob("platform/*")): + # Only platforms that opt in to vs proj generation are included. + if not os.path.isdir(x) or not os.path.exists(x + "/msvs.py"): + continue + tmppath = "./" + x + sys.path.insert(0, tmppath) + import msvs + + vs_plats = [] + vs_confs = [] + try: + platform_name = x[9:] + vs_plats = msvs.get_platforms() + vs_confs = msvs.get_configurations() + val = [] + for plat in vs_plats: + val += [{"platform": plat[0], "architecture": plat[1]}] + + vsconf = {"platform": platform_name, "targets": vs_confs, "arches": val} + confs += [vsconf] + + # Save additional information about the configuration for the actively selected platform, + # so we can generate the platform-specific props file with all the build commands/defines/etc + if platform == platform_name: + common_build_prefix = msvs.get_build_prefix(env) + vs_configuration = vsconf + except Exception: + pass + + sys.path.remove(tmppath) + sys.modules.pop("msvs") + + headers = [] + headers_dirs = [] + for file in glob_recursive_2("*.h", headers_dirs): + headers.append(str(file).replace("/", "\\")) + for file in glob_recursive_2("*.hpp", headers_dirs): + headers.append(str(file).replace("/", "\\")) + + sources = [] + sources_dirs = [] + for file in glob_recursive_2("*.cpp", sources_dirs): + sources.append(str(file).replace("/", "\\")) + for file in glob_recursive_2("*.c", sources_dirs): + sources.append(str(file).replace("/", "\\")) + + others = [] + others_dirs = [] + for file in glob_recursive_2("*.natvis", others_dirs): + others.append(str(file).replace("/", "\\")) + for file in glob_recursive_2("*.glsl", others_dirs): + others.append(str(file).replace("/", "\\")) + + skip_filters = False + import hashlib + import json + + md5 = hashlib.md5( + json.dumps(headers + headers_dirs + sources + sources_dirs + others + others_dirs, sort_keys=True).encode( + "utf-8" + ) + ).hexdigest() + + if os.path.exists(f"{project_name}.vcxproj.filters"): + existing_filters = open(f"{project_name}.vcxproj.filters", "r").read() + match = re.search(r"(?ms)^ \ No newline at end of file diff --git a/misc/msvs/vcxproj.template b/misc/msvs/vcxproj.template new file mode 100644 index 00000000000..a1cf22bfb9b --- /dev/null +++ b/misc/msvs/vcxproj.template @@ -0,0 +1,42 @@ + + + + %%CONFS%% + + + {%%UUID%%} + godot + MakeFileProj + NoUpgrade + + + %%PROPERTIES%% + + + + Makefile + false + v143 + $(SolutionDir)\bin\$(Platform)\$(Configuration)\ + obj\$(Platform)\$(Configuration)\ + $(OutDir)\Layout + + + + + + + + + + <_ProjectFileVersion>10.0.30319.1 + + + %%IMPORTS%% + + %%DEFAULT_ITEMS%% + + + + + \ No newline at end of file diff --git a/platform/windows/SCsub b/platform/windows/SCsub index 7aaf70e6256..6010d4ba767 100644 --- a/platform/windows/SCsub +++ b/platform/windows/SCsub @@ -7,6 +7,8 @@ from pathlib import Path from platform_methods import run_in_subprocess import platform_windows_builders +sources = [] + common_win = [ "godot_windows.cpp", "crash_handler_windows.cpp", @@ -43,7 +45,8 @@ res_file = "godot_res.rc" res_target = "godot_res" + env["OBJSUFFIX"] res_obj = env.RES(res_target, res_file) -sources = common_win + res_obj +env.add_source_files(sources, common_win) +sources += res_obj prog = env.add_program("#bin/godot", sources, PROGSUFFIX=env["PROGSUFFIX"]) arrange_program_clean(prog) @@ -65,6 +68,7 @@ if env["windows_subsystem"] == "gui": prog_wrap = env_wrap.add_program("#bin/godot", common_win_wrap + res_wrap_obj, PROGSUFFIX=env["PROGSUFFIX_WRAP"]) arrange_program_clean(prog_wrap) env_wrap.Depends(prog_wrap, prog) + sources += common_win_wrap + res_wrap_obj # Microsoft Visual Studio Project Generation if env["vsproj"]: @@ -134,3 +138,5 @@ if not os.getenv("VCINSTALLDIR"): env.AddPostAction(prog, run_in_subprocess(platform_windows_builders.make_debug_mingw)) if env["windows_subsystem"] == "gui": env.AddPostAction(prog_wrap, run_in_subprocess(platform_windows_builders.make_debug_mingw)) + +env.platform_sources += sources diff --git a/platform/windows/msvs.py b/platform/windows/msvs.py new file mode 100644 index 00000000000..2d5ebe811a4 --- /dev/null +++ b/platform/windows/msvs.py @@ -0,0 +1,20 @@ +import methods + + +# Tuples with the name of the arch that will be used in VS, mapped to our internal arch names. +# For Windows platforms, Win32 is what VS wants. For other platforms, it can be different. +def get_platforms(): + return [("Win32", "x86_32"), ("x64", "x86_64")] + + +def get_configurations(): + return ["editor", "template_debug", "template_release"] + + +def get_build_prefix(env): + batch_file = methods.find_visual_c_batch_file(env) + return [ + "set "plat=$(PlatformTarget)"", + "(if "$(PlatformTarget)"=="x64" (set "plat=x86_amd64"))", + f"call "{batch_file}" !plat!", + ]