diff options
Diffstat (limited to 'methods.py')
-rw-r--r-- | methods.py | 608 |
1 files changed, 453 insertions, 155 deletions
diff --git a/methods.py b/methods.py index f36591d211..c22b1f11e4 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)^<!-- 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="$(ProjectDir.TrimEnd('\\'))"", + "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 && cmd /V /C " + commands + else: + common_build_prefix[0] = "echo Starting SCons && cmd /V /C " + common_build_prefix[0] + + cmd = " ^& ".join(common_build_prefix + [" ".join([commands] + common_build_postfix)]) + props_template = props_template.replace("%%BUILD%%", cmd) + + cmd = " ^& ".join(common_build_prefix + [" ".join([commands] + cmd_rebuild)]) + props_template = props_template.replace("%%REBUILD%%", cmd) + + cmd = " ^& ".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() |