浏览代码

Merge pull request #84885 from shana/vsproj-for-everyone

New VS proj generation logic that supports any platform that wants to opt in
Rémi Verschelde 1 年之前
父节点
当前提交
bbccd95d22

+ 1 - 1
.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)

+ 1 - 0
.gitignore

@@ -367,3 +367,4 @@ $RECYCLE.BIN/
 *.msm
 *.msp
 *.lnk
+*.generated.props

+ 0 - 3
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")

+ 453 - 155
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)^<!-- CHECKSUM$.([0-9a-f]{32})", existing_filters)
+        if match is not None and md5 == match.group(1):
+            skip_filters = True
+
+    import uuid
+
+    # Don't regenerate the filters file if nothing has changed, so we keep the existing UUIDs.
+    if not skip_filters:
+        print(f"Regenerating {project_name}.vcxproj.filters")
+
+        filters_template = open("misc/msvs/vcxproj.filters.template", "r").read()
+        for i in range(1, 10):
+            filters_template = filters_template.replace(f"%%UUID{i}%%", str(uuid.uuid4()))
+
+        filters = ""
+
+        for d in headers_dirs:
+            filters += f'<Filter Include="Header Files\\{d}"><UniqueIdentifier>{{{str(uuid.uuid4())}}}</UniqueIdentifier></Filter>\n'
+        for d in sources_dirs:
+            filters += f'<Filter Include="Source Files\\{d}"><UniqueIdentifier>{{{str(uuid.uuid4())}}}</UniqueIdentifier></Filter>\n'
+        for d in others_dirs:
+            filters += f'<Filter Include="Other Files\\{d}"><UniqueIdentifier>{{{str(uuid.uuid4())}}}</UniqueIdentifier></Filter>\n'
+
+        filters_template = filters_template.replace("%%FILTERS%%", filters)
+
+        filters = ""
+        for file in headers:
+            filters += (
+                f'<ClInclude Include="{file}"><Filter>Header Files\\{os.path.dirname(file)}</Filter></ClInclude>\n'
+            )
+        filters_template = filters_template.replace("%%INCLUDES%%", filters)
+
+        filters = ""
+        for file in sources:
+            filters += (
+                f'<ClCompile Include="{file}"><Filter>Source Files\\{os.path.dirname(file)}</Filter></ClCompile>\n'
+            )
+
+        filters_template = filters_template.replace("%%COMPILES%%", filters)
+
+        filters = ""
+        for file in others:
+            filters += f'<None Include="{file}"><Filter>Other Files\\{os.path.dirname(file)}</Filter></None>\n'
+        filters_template = filters_template.replace("%%OTHERS%%", filters)
+
+        filters_template = filters_template.replace("%%HASH%%", md5)
+
+        with open(f"{project_name}.vcxproj.filters", "w") as f:
+            f.write(filters_template)
+
+    envsources = []
+
+    envsources += env.core_sources
+    envsources += env.drivers_sources
+    envsources += env.main_sources
+    envsources += env.modules_sources
+    envsources += env.scene_sources
+    envsources += env.servers_sources
+    if env.editor_build:
+        envsources += env.editor_sources
+    envsources += env.platform_sources
+
+    headers_active = []
+    sources_active = []
+    others_active = []
+    for x in envsources:
+        fname = ""
+        if type(x) == type(""):
+            fname = env.File(x).path
+        else:
+            # Some object files might get added directly as a File object and not a list.
+            try:
+                fname = env.File(x)[0].path
+            except:
+                fname = x.path
+                pass
+
+        if fname:
+            fname = fname.replace("\\\\", "/")
+            parts = os.path.splitext(fname)
+            basename = parts[0]
+            ext = parts[1]
+            idx = fname.find(env["OBJSUFFIX"])
+            if ext in [".h", ".hpp"]:
+                headers_active += [fname]
+            elif ext in [".c", ".cpp"]:
+                sources_active += [fname]
+            elif idx > 0:
+                basename = fname[:idx]
+                if os.path.isfile(basename + ".h"):
+                    headers_active += [basename + ".h"]
+                elif os.path.isfile(basename + ".hpp"):
+                    headers_active += [basename + ".hpp"]
+                elif basename.endswith(".gen") and os.path.isfile(basename[:-4] + ".h"):
+                    headers_active += [basename[:-4] + ".h"]
+                if os.path.isfile(basename + ".c"):
+                    sources_active += [basename + ".c"]
+                elif os.path.isfile(basename + ".cpp"):
+                    sources_active += [basename + ".cpp"]
+            else:
+                fname = os.path.relpath(os.path.abspath(fname), env.Dir("").abspath)
+                others_active += [fname]
+
+    all_items = []
+    properties = []
+    activeItems = []
+    extraItems = []
+
+    set_headers = set(headers_active)
+    set_sources = set(sources_active)
+    set_others = set(others_active)
+    for file in headers:
+        all_items.append(f'<ClInclude Include="{file}">')
+        all_items.append(
+            f"  <ExcludedFromBuild Condition=\"!$(ActiveProjectItemList.Contains(';{file};'))\">true</ExcludedFromBuild>"
+        )
+        all_items.append("</ClInclude>")
+        if file in set_headers:
+            activeItems.append(file)
+
+    for file in sources:
+        all_items.append(f'<ClCompile Include="{file}">')
+        all_items.append(
+            f"  <ExcludedFromBuild Condition=\"!$(ActiveProjectItemList.Contains(';{file};'))\">true</ExcludedFromBuild>"
+        )
+        all_items.append("</ClCompile>")
+        if file in set_sources:
+            activeItems.append(file)
+
+    for file in others:
+        all_items.append(f'<None Include="{file}">')
+        all_items.append(
+            f"  <ExcludedFromBuild Condition=\"!$(ActiveProjectItemList.Contains(';{file};'))\">true</ExcludedFromBuild>"
+        )
+        all_items.append("</None>")
+        if file in set_others:
+            activeItems.append(file)
+
+    if vs_configuration:
+        vsconf = ""
+        for a in vs_configuration["arches"]:
+            if arch == a["architecture"]:
+                vsconf = f'{target}|{a["platform"]}'
+                break
+
+        condition = "'$(Configuration)|$(Platform)'=='" + vsconf + "'"
+        properties.append("<ActiveProjectItemList>;" + ";".join(activeItems) + ";</ActiveProjectItemList>")
+        output = f'bin\\godot{env["PROGSUFFIX"]}'
+
+        props_template = open("misc/msvs/props.template", "r").read()
+
+        props_template = props_template.replace("%%VSCONF%%", vsconf)
+        props_template = props_template.replace("%%CONDITION%%", condition)
+        props_template = props_template.replace("%%PROPERTIES%%", "\n    ".join(properties))
+        props_template = props_template.replace("%%EXTRA_ITEMS%%", "\n    ".join(extraItems))
+
+        props_template = props_template.replace("%%OUTPUT%%", output)
+
+        props_template = props_template.replace(
+            "%%DEFINES%%", ";".join([format_key_value(v) for v in list(env["CPPDEFINES"])])
+        )
+        props_template = props_template.replace("%%INCLUDES%%", ";".join([str(j) for j in env["CPPPATH"]]))
+        props_template = props_template.replace(
+            "%%OPTIONS%%",
+            " ".join(env["CCFLAGS"]) + " " + " ".join([x for x in env["CXXFLAGS"] if not x.startswith("$")]),
+        )
+
+        # 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=&quot;$(ProjectDir.TrimEnd(&apos;\\&apos;))&quot;",
+            "progress=no",
+            f"platform={platform}",
+            f"target={target}",
+            f"arch={arch}",
+        ]
+
+        for arg, value in filtered_args.items():
+            common_build_postfix.append(f"{arg}={value}")
+
+        cmd_rebuild = [
+            "vsproj=yes",
+            f"vsproj_name={project_name}",
+        ] + common_build_postfix
+
+        cmd_clean = [
+            "--clean",
+        ] + common_build_postfix
+
+        commands = "scons"
+        if len(common_build_prefix) == 0:
+            commands = "echo Starting SCons &amp;&amp; cmd /V /C " + commands
+        else:
+            common_build_prefix[0] = "echo Starting SCons &amp;&amp; cmd /V /C " + common_build_prefix[0]
+
+        cmd = " ^&amp; ".join(common_build_prefix + [" ".join([commands] + common_build_postfix)])
+        props_template = props_template.replace("%%BUILD%%", cmd)
+
+        cmd = " ^&amp; ".join(common_build_prefix + [" ".join([commands] + cmd_rebuild)])
+        props_template = props_template.replace("%%REBUILD%%", cmd)
+
+        cmd = " ^&amp; ".join(common_build_prefix + [" ".join([commands] + cmd_clean)])
+        props_template = props_template.replace("%%CLEAN%%", cmd)
+
+        with open(f"{project_name}.{platform}.{target}.{arch}.generated.props", "w") as f:
+            f.write(props_template)
+
+    proj_uuid = str(uuid.uuid4())
+    sln_uuid = str(uuid.uuid4())
+
+    if os.path.exists(f"{project_name}.sln"):
+        for line in open(f"{project_name}.sln", "r").read().splitlines():
+            if line.startswith('Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}")'):
+                proj_uuid = re.search(
+                    r"\"{(\b[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-\b[0-9a-fA-F]{12}\b)}\"$",
+                    line,
+                ).group(1)
+            elif line.strip().startswith("SolutionGuid ="):
+                sln_uuid = re.search(
+                    r"{(\b[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-\b[0-9a-fA-F]{12}\b)}", line
+                ).group(1)
+                break
+
+    configurations = []
+    imports = []
+    properties = []
+    section1 = []
+    section2 = []
+    for conf in confs:
+        godot_platform = conf["platform"]
+        for p in conf["arches"]:
+            sln_plat = p["platform"]
+            proj_plat = sln_plat
+            godot_arch = p["architecture"]
+
+            # Redirect editor configurations for non-Windows platforms to the Windows one, so the solution has all the permutations
+            # and VS doesn't complain about missing project configurations.
+            # These configurations are disabled, so they show up but won't build.
+            if godot_platform != "windows":
+                section1 += [f"editor|{sln_plat} = editor|{proj_plat}"]
+                section2 += [
+                    f"{{{proj_uuid}}}.editor|{proj_plat}.ActiveCfg = editor|{proj_plat}",
+                ]
+
+            for t in conf["targets"]:
+                godot_target = t
+
+                # Windows x86 is a special little flower that requires a project platform == Win32 but a solution platform == x86.
+                if godot_platform == "windows" and godot_target == "editor" and godot_arch == "x86_32":
+                    sln_plat = "x86"
+
+                configurations += [
+                    f'<ProjectConfiguration Include="{godot_target}|{proj_plat}">',
+                    f"  <Configuration>{godot_target}</Configuration>",
+                    f"  <Platform>{proj_plat}</Platform>",
+                    "</ProjectConfiguration>",
+                ]
+
+                if godot_platform != "windows":
+                    configurations += [
+                        f'<ProjectConfiguration Include="editor|{proj_plat}">',
+                        f"  <Configuration>editor</Configuration>",
+                        f"  <Platform>{proj_plat}</Platform>",
+                        "</ProjectConfiguration>",
+                    ]
+
+                p = f"{project_name}.{godot_platform}.{godot_target}.{godot_arch}.generated.props"
+                imports += [
+                    f'<Import Project="$(MSBuildProjectDirectory)\\{p}" Condition="Exists(\'$(MSBuildProjectDirectory)\\{p}\')"/>'
+                ]
+
+                section1 += [f"{godot_target}|{sln_plat} = {godot_target}|{sln_plat}"]
+
+                section2 += [
+                    f"{{{proj_uuid}}}.{godot_target}|{sln_plat}.ActiveCfg = {godot_target}|{proj_plat}",
+                    f"{{{proj_uuid}}}.{godot_target}|{sln_plat}.Build.0 = {godot_target}|{proj_plat}",
+                ]
+
+    section1 = sorted(section1)
+    section2 = sorted(section2)
+
+    proj_template = open("misc/msvs/vcxproj.template", "r").read()
+
+    proj_template = proj_template.replace("%%UUID%%", proj_uuid)
+    proj_template = proj_template.replace("%%CONFS%%", "\n    ".join(configurations))
+    proj_template = proj_template.replace("%%IMPORTS%%", "\n  ".join(imports))
+    proj_template = proj_template.replace("%%DEFAULT_ITEMS%%", "\n    ".join(all_items))
+    proj_template = proj_template.replace("%%PROPERTIES%%", "\n  ".join(properties))
+
+    with open(f"{project_name}.vcxproj", "w") as f:
+        f.write(proj_template)
+
+    sln_template = open("misc/msvs/sln.template", "r").read()
+    sln_template = sln_template.replace("%%NAME%%", project_name)
+    sln_template = sln_template.replace("%%UUID%%", proj_uuid)
+    sln_template = sln_template.replace("%%SLNUUID%%", sln_uuid)
+    sln_template = sln_template.replace("%%SECTION1%%", "\n    ".join(section1))
+    sln_template = sln_template.replace("%%SECTION2%%", "\n    ".join(section2))
+    with open(f"{project_name}.sln", "w") as f:
+        f.write(sln_template)
+
+    if get_bool(original_args, "vsproj_gen_only", True):
+        sys.exit()

+ 21 - 0
misc/msvs/props.template

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='%%VSCONF%%'">
+    <NMakeBuildCommandLine>%%BUILD%%</NMakeBuildCommandLine>
+    <NMakeReBuildCommandLine>%%REBUILD%%</NMakeReBuildCommandLine>
+    <NMakeCleanCommandLine>%%CLEAN%%</NMakeCleanCommandLine>
+    <NMakeOutput>%%OUTPUT%%</NMakeOutput>
+    <NMakePreprocessorDefinitions>%%DEFINES%%</NMakePreprocessorDefinitions>
+    <NMakeIncludeSearchPath>%%INCLUDES%%</NMakeIncludeSearchPath>
+    <NMakeForcedIncludes>$(NMakeForcedIncludes)</NMakeForcedIncludes>
+    <NMakeAssemblySearchPath>$(NMakeAssemblySearchPath)</NMakeAssemblySearchPath>
+    <NMakeForcedUsingAssemblies>$(NMakeForcedUsingAssemblies)</NMakeForcedUsingAssemblies>
+    <AdditionalOptions>%%OPTIONS%%</AdditionalOptions>
+  </PropertyGroup>
+  <PropertyGroup Condition="%%CONDITION%%">
+    %%PROPERTIES%%
+  </PropertyGroup>
+  <ItemGroup Condition="%%CONDITION%%">
+    %%EXTRA_ITEMS%%
+  </ItemGroup>
+</Project>

+ 20 - 0
misc/msvs/sln.template

@@ -0,0 +1,20 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.7.34221.43
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "%%NAME%%", "%%NAME%%.vcxproj", "{%%UUID%%}"
+EndProject
+Global
+  GlobalSection(SolutionConfigurationPlatforms) = preSolution
+    %%SECTION1%%
+  EndGlobalSection
+  GlobalSection(ProjectConfigurationPlatforms) = postSolution
+    %%SECTION2%%
+  EndGlobalSection
+  GlobalSection(SolutionProperties) = preSolution
+    HideSolutionNode = FALSE
+  EndGlobalSection
+  GlobalSection(ExtensibilityGlobals) = postSolution
+    SolutionGuid = {%%SLNUUID%%}
+  EndGlobalSection
+EndGlobal

+ 30 - 0
misc/msvs/vcxproj.filters.template

@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <ItemGroup>
+    <Filter Include="Source Files">
+      <UniqueIdentifier>%%UUID1%%</UniqueIdentifier>
+    </Filter>
+    <Filter Include="Header Files">
+      <UniqueIdentifier>%%UUID2%%</UniqueIdentifier>
+    </Filter>
+    <Filter Include="Resource Files">
+      <UniqueIdentifier>%%UUID3%%</UniqueIdentifier>
+    </Filter>
+    <Filter Include="Scripts">
+      <UniqueIdentifier>%%UUID4%%</UniqueIdentifier>
+    </Filter>
+    %%FILTERS%%
+  </ItemGroup>
+  <ItemGroup>
+    %%COMPILES%%
+  </ItemGroup>
+  <ItemGroup>
+    %%INCLUDES%%
+  </ItemGroup>
+  <ItemGroup>
+    %%OTHERS%%
+  </ItemGroup>
+</Project>
+<!-- CHECKSUM
+%%HASH%%
+-->

+ 42 - 0
misc/msvs/vcxproj.template

@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <ItemGroup Label="ProjectConfigurations">
+    %%CONFS%%
+  </ItemGroup>
+  <PropertyGroup Label="Globals">
+    <ProjectGuid>{%%UUID%%}</ProjectGuid>
+    <RootNamespace>godot</RootNamespace>
+    <Keyword>MakeFileProj</Keyword>
+    <VCProjectUpgraderObjectName>NoUpgrade</VCProjectUpgraderObjectName>
+  </PropertyGroup>
+  <PropertyGroup>
+    %%PROPERTIES%%
+  </PropertyGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
+  <PropertyGroup Label="Configuration">
+    <ConfigurationType>Makefile</ConfigurationType>
+    <UseOfMfc>false</UseOfMfc>
+    <PlatformToolset>v143</PlatformToolset>
+    <OutDir>$(SolutionDir)\bin\$(Platform)\$(Configuration)\</OutDir>
+    <IntDir>obj\$(Platform)\$(Configuration)\</IntDir>
+    <LayoutDir>$(OutDir)\Layout</LayoutDir>
+  </PropertyGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
+  <ImportGroup Label="ExtensionSettings">
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+  </ImportGroup>
+  <PropertyGroup Label="UserMacros" />
+  <PropertyGroup>
+    <_ProjectFileVersion>10.0.30319.1</_ProjectFileVersion>
+    <ActiveProjectItemList></ActiveProjectItemList>
+  </PropertyGroup>
+  %%IMPORTS%%
+  <ItemGroup Condition="'$(IncludeListImported)'==''">
+    %%DEFAULT_ITEMS%%
+  </ItemGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
+  <ImportGroup Label="ExtensionTargets">
+  </ImportGroup>
+</Project>

+ 7 - 1
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

+ 20 - 0
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 &quot;plat=$(PlatformTarget)&quot;",
+        "(if &quot;$(PlatformTarget)&quot;==&quot;x64&quot; (set &quot;plat=x86_amd64&quot;))",
+        f"call &quot;{batch_file}&quot; !plat!",
+    ]