From 7638a6c9811590a384e2126dd004e302f76d3e4a Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Tue, 14 Nov 2023 13:39:44 +0100 Subject: [PATCH] Add new VS proj generation logic that supports any platform that wants to opt in 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` To generate build configuration files for all platforms+targets+arch combinations, users should call ``` scons vsproj=yes platform=XXX target=YYY [other build flags] ``` for each combination of platform+target[+arch]. This will generate the relevant vs project files but skip the build process, so that project files can be quickly generated without waiting for a command line build. This lets project files be quickly generated even if there are build errors. All possible combinations of platform+target are created in the solution file by default, but they won't do anything until each one is set up with a scons vsproj=yes command for the respective platform in the appropriate command line. This lets users only generate the combinations they need, and VS won't have to parse settings for other combos. 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 currently being processed by scons. 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 AND build from the command line, run ``` scons vsproj=yes vsproj_gen_only=no ``` --- .github/workflows/windows_builds.yml | 2 +- .gitignore | 1 + SConstruct | 3 - methods.py | 608 ++++++++++++++++++++------- misc/msvs/props.template | 21 + misc/msvs/sln.template | 20 + misc/msvs/vcxproj.filters.template | 30 ++ misc/msvs/vcxproj.template | 42 ++ platform/windows/SCsub | 8 +- platform/windows/msvs.py | 20 + 10 files changed, 595 insertions(+), 160 deletions(-) create mode 100644 misc/msvs/props.template create mode 100644 misc/msvs/sln.template create mode 100644 misc/msvs/vcxproj.filters.template create mode 100644 misc/msvs/vcxproj.template create mode 100644 platform/windows/msvs.py 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!", + ]