Last active
October 21, 2024 14:18
-
-
Save max-dark/c81cd849e1e2da544ae1366cb3925152 to your computer and use it in GitHub Desktop.
convert vc project to cmake
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # add target from generated "*.cmake" | |
| function(fill_project VC_PROJECT_FILE CONFIG_NAME) | |
| set(_project_file "${VC_PROJECT_FILE}.cmake") | |
| set(_all_sources) | |
| cmake_path(GET _project_file PARENT_PATH _project_path) | |
| if ("${_project_path}" STREQUAL "") | |
| set(_project_path "./") | |
| endif() | |
| include("${_project_file}") | |
| # TODO: move var to generated file | |
| set(_ConfigurationName "${CMAKE_BUILD_TYPE}") | |
| set(_OutDir "${CMAKE_CURRENT_BINARY_DIR}") | |
| set(_ProjectDir "${_project_path}") | |
| set(_SolutionDir "${CMAKE_CURRENT_LIST_DIR}") | |
| set(_ProjectName "${VC_project}") | |
| set(_TargetDir "${_OutDir}") | |
| set(_TargetName "${_ProjectName}") | |
| include("${_project_file}") | |
| message("Add project '${VC_project}' from ${_project_path}, nConfigs == ${VC_num_configs}") | |
| foreach(IDX RANGE 1 ${VC_num_groups}) | |
| math(EXPR IDX "${IDX} - 1") | |
| set(_base "VC_flies_${IDX}") | |
| set(_name "${${_base}_name}") | |
| set(_list "${${_base}_list}") | |
| set(_tmp) | |
| foreach(_fname IN LISTS _list) | |
| set(_file_name "${_project_path}/${_fname}") | |
| list(APPEND _tmp "${_file_name}") | |
| list(APPEND _all_sources "${_file_name}") | |
| endforeach() | |
| message("Add group ${_name}") | |
| source_group(${_name} FILES ${_tmp}) | |
| endforeach() | |
| foreach(IDX RANGE 1 ${VC_num_configs}) | |
| math(EXPR IDX "${IDX} - 1") | |
| set(_project "${VC_project}") | |
| set(_cfg_base "VC_config_${IDX}") | |
| set(_cfg_full_name "${${_cfg_base}_full_name}") | |
| set(_cfg_name "${${_cfg_base}_name}") | |
| set(_cfg_type "${${_cfg_base}_type}") | |
| set(_cfg_subsys "${${_cfg_base}_subsystem}") | |
| set(_cfg_charset "${${_cfg_base}_charset}") | |
| message("${IDX} - ${_cfg_full_name} / ${_cfg_type}") | |
| if (NOT _cfg_full_name STREQUAL "${CONFIG_NAME}") | |
| message("Skip '${_cfg_full_name}") | |
| continue() | |
| endif() | |
| if (_cfg_type STREQUAL "EXE") | |
| add_executable("${_project}") | |
| if (_cfg_charset STREQUAL "2") | |
| set_property(TARGET "${_project}" PROPERTY WIN32_EXECUTABLE) | |
| endif() | |
| elseif(_cfg_type STREQUAL "DLL") | |
| add_library("${_project}" SHARED) | |
| elseif(_cfg_type STREQUAL "LIB") | |
| add_library("${_project}" STATIC) | |
| else() | |
| message(ERROR "'${_cfg_type}' is unknown") | |
| endif() | |
| set(_include_dirs "${${_cfg_base}_include_dirs}") | |
| set(_library_dirs "${${_cfg_base}_library_dirs}") | |
| set(_defines "${${_cfg_base}_defines}") | |
| set(_libraries "${${_cfg_base}_libraries}") | |
| target_compile_definitions( | |
| "${_project}" | |
| PRIVATE | |
| ${_defines} | |
| ) | |
| target_include_directories( | |
| "${_project}" | |
| PRIVATE | |
| ${_include_dirs} | |
| ) | |
| target_link_directories( | |
| "${_project}" | |
| PRIVATE | |
| ${_library_dirs} | |
| ) | |
| target_sources( | |
| "${_project}" | |
| PRIVATE | |
| ${_all_sources} | |
| ) | |
| unset(_cfg_type) | |
| unset(_cfg_name) | |
| unset(_cfg_base) | |
| endforeach() | |
| endfunction(fill_project) | |
| # usage: | |
| # | |
| # set(CURRENT_CFG "Debug|Win32") | |
| # | |
| # fill_project(my_app/my_app.vcproj "${CURRENT_CFG}") | |
| # fill_project(my_project/my_project.vcproj "${CURRENT_CFG}") | |
| # fill_project(my_dll/my_dll.vcproj "${CURRENT_CFG}") | |
| # fill_project(my_lib/my_lib.vcproj "${CURRENT_CFG}") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| /usr/bin/find . -name "*.vcproj" -exec python vc2cmake.py --input {} \; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import xml.etree.ElementTree as ET | |
| import os | |
| import sys | |
| import argparse | |
| import json | |
| def make_arg_parser(name: str) -> argparse.ArgumentParser: | |
| parser = argparse.ArgumentParser( | |
| prog=name | |
| ) | |
| parser.add_argument("--input") | |
| parser.add_argument("--output") | |
| parser.add_argument("--encoding") | |
| parser.add_argument("--print", action="store_true") | |
| return parser | |
| def fix_path(raw: str) -> str: | |
| fix = raw.replace("\\","/") | |
| return fix.removeprefix('./') # .replace(""","\"") | |
| def to_list(raw: str) -> list[str]: | |
| return | |
| def is_empty(raw: str|None, whitespace:bool = True) -> bool: | |
| return bool((not raw) or (whitespace and raw.isspace())) | |
| def quote(val:str, multiline:bool = False) -> str: | |
| if multiline: | |
| return '[=[\n{}\n]=]'.format(val) | |
| return '"{}"'.format(val) | |
| def parse_path_list(raw: str) -> list[str]: | |
| if is_empty(raw): | |
| return list() | |
| raw_fix = fix_path(raw) | |
| return raw_fix.split(';') | |
| def load_vcprj(path: str, encoding:str|None) -> ET.Element: | |
| xml = "" | |
| with open(path, encoding=encoding) as project: | |
| xml = project.read() | |
| tree = ET.fromstring(xml) | |
| return tree | |
| def attr(elem: ET.Element, key:str, default:str='') -> str: | |
| value = elem.attrib.get(key) | |
| return default if value is None else value | |
| class BuildConfig: | |
| def __init__(self): | |
| self.full_name = "" | |
| self.name = "" | |
| self.platform = "" | |
| self.out_name = "" | |
| self.config_type = "EXE" | |
| self.charset = "1" | |
| self.subsystem = "1" | |
| self.include_dirs: list[str] = list() | |
| self.defines: list[str] = list() | |
| self.library_dirs: list[str] = list() | |
| self.libraries: list[str] = list() | |
| self.custom_steps: dict[str, str] = dict() | |
| self.unknown_tools: list[str] = list() | |
| def fill(xml:ET.Element): | |
| pass | |
| def set_name(self, full_name: str) -> None: | |
| id, platform = full_name.split('|', 2) | |
| self.full_name = full_name | |
| self.name = id | |
| self.platform = platform | |
| def set_type(self, value:str) -> None: | |
| match value: | |
| case "1": | |
| self.config_type = "EXE" | |
| case "2": | |
| self.config_type = "DLL" | |
| case "4": | |
| self.config_type = "LIB" | |
| case _: | |
| pass | |
| def set_custom(self, id:str, cmd:str): | |
| if is_empty(id) or is_empty(cmd): | |
| return | |
| self.custom_steps[id] = cmd.replace("\r\n", "\n") | |
| def __str__(self): | |
| return json.dumps(self.__dict__) | |
| def on_compiler(self, tool: ET.Element): | |
| attr_include_dir = attr(tool, 'AdditionalIncludeDirectories') | |
| attr_defines = attr(tool, 'PreprocessorDefinitions') | |
| self.include_dirs = parse_path_list(attr_include_dir) | |
| self.defines = attr_defines.split(';') | |
| def on_linker(self, tool: ET.Element): | |
| attr_library_dirs = attr(tool, 'AdditionalLibraryDirectories') | |
| attr_libraries = attr(tool, 'AdditionalDependencies') | |
| attr_output = attr(tool, 'OutputFile') | |
| self.subsystem = attr(tool, 'SubSystem', '1') | |
| self.out_name = fix_path(attr_output) | |
| self.libraries = attr_libraries.split() | |
| self.library_dirs = parse_path_list(attr_library_dirs) | |
| def on_pre_build(self, tool: ET.Element): | |
| attr_command = attr(tool, 'CommandLine') | |
| self.set_custom('pre_build', attr_command) | |
| def on_pre_link(self, tool: ET.Element): | |
| attr_command = attr(tool, 'CommandLine') | |
| self.set_custom('pre_link', attr_command) | |
| def on_post_build(self, tool: ET.Element): | |
| attr_command = attr(tool, 'CommandLine') | |
| self.set_custom('post_build', attr_command) | |
| def on_custom_build(self, tool: ET.Element): | |
| attr_command = attr(tool, 'CommandLine') | |
| self.set_custom('custom_tool', attr_command) | |
| def on_unknown(self, tool: ET.Element, tool_id: str): | |
| self.unknown_tools.append(tool_id) | |
| def parse_tool(self, tool: ET.Element): | |
| tool_id = tool.attrib['Name'] | |
| match tool_id: | |
| # case '': | |
| # pass | |
| case 'VCPreBuildEventTool': | |
| self.on_pre_build(tool) | |
| case 'VCCustomBuildTool': | |
| self.on_custom_build(tool) | |
| case 'VCCLCompilerTool': | |
| self.on_compiler(tool) | |
| case 'VCPreLinkEventTool': | |
| self.on_pre_link(tool) | |
| case 'VCLinkerTool': | |
| self.on_linker(tool) | |
| case 'VCLibrarianTool': | |
| self.on_linker(tool) | |
| case 'VCPostBuildEventTool': | |
| self.on_post_build(tool) | |
| case _: | |
| self.on_unknown(tool, tool_id) | |
| class FileConfig: | |
| def __init__(self): | |
| self.file_path:str = "" | |
| self.have_configs: bool = False | |
| def parse_config(self, xml_cfg: ET.Element): | |
| self.have_configs = True | |
| pass | |
| def parse(self, xml_cfg: ET.Element): | |
| self.file_path = fix_path(xml_cfg.attrib['RelativePath']) | |
| for cfg in xml_cfg.findall('FileConfiguration'): | |
| self.parse_config(cfg) | |
| def __str__(self): | |
| return json.dumps(self.__dict__) | |
| def parse_config(xml_cfg: ET.Element, config_name: str) -> BuildConfig: | |
| cfg = BuildConfig() | |
| cfg.set_name(config_name) | |
| cfg.set_type(attr(xml_cfg, "ConfigurationType")) | |
| cfg.charset = attr(xml_cfg, 'CharacterSet', '1') | |
| for tool in xml_cfg.findall("Tool"): | |
| cfg.parse_tool(tool) | |
| return cfg | |
| def parse_filter(file_groups: dict[str, list[FileConfig]], xml: ET.Element, root_name:str = "") -> list[FileConfig]: | |
| files = list() | |
| filter_name = xml.attrib['Name'] | |
| for item in xml.findall("File"): | |
| file_cfg = FileConfig() | |
| file_cfg.parse(item) | |
| files.append(file_cfg) | |
| current = "{}/{}".format(root_name, filter_name) | |
| file_groups.setdefault(current, []) | |
| file_groups[current] += files | |
| for filter in xml.findall("Filter"): | |
| parse_filter(file_groups, filter, current) | |
| def to_cmake(xml: ET.Element) -> str: | |
| script:list[str] = [] | |
| var_prefix = "VC_{}" | |
| def add(text:str): | |
| script.append(text) | |
| def variable(name:str) -> str: | |
| return var_prefix.format(name) | |
| def var_set(name:str, value: str|None) -> str: | |
| val_fmt = "" if is_empty(value) else "\n\t{}\n".format(value) | |
| return "set({}{})".format(variable(name), val_fmt) | |
| def var_del(name:str) -> str: | |
| return "unset({})".format(variable(name)) | |
| def list_add(name:str, value:list[str]|None) -> str: | |
| val_str = '\n\t'.join(map( | |
| lambda x: '"{}"'.format(x.strip('"')), | |
| value | |
| )) | |
| var = variable(name) | |
| val_fmt = "" if is_empty(val_str) else "\n\t{}\n".format(val_str) | |
| return "list(APPEND {}{})".format(var, val_fmt) | |
| def list_set(name:str, value:list[str]|None) -> str: | |
| return list_add(name, value) | |
| project_name = xml.attrib['Name'] | |
| platforms: list[str] = list() | |
| build_cfgs: dict[str,BuildConfig] = dict() | |
| file_groups: dict[str, list[FileConfig]] = dict() | |
| for platform in xml.findall('Platforms/Platform'): | |
| platforms.append(platform.attrib['Name']) | |
| for config in xml.findall("Configurations/Configuration"): | |
| config_name = config.attrib['Name'] | |
| build_cfgs[config_name] = parse_config(config, config_name) | |
| for filter in xml.findall("Files/Filter"): | |
| parse_filter(file_groups, filter) | |
| add("# Project name") | |
| add(var_set("project", project_name)) | |
| add("# Suppeorted platforms") | |
| add(var_set("platforms", None)) | |
| add(list_add("platforms", platforms)) | |
| num_cfg = len(build_cfgs) | |
| add("# Build configurations") | |
| add(var_set("num_configs", str(num_cfg))) | |
| cfg_idx = 0 | |
| for k in build_cfgs: | |
| add("# config '{}'".format(k)) | |
| var_base = 'config_{}_{}'.format(cfg_idx, '{}') | |
| v = build_cfgs[k] | |
| add(var_set(var_base.format('full_name'), quote(v.full_name))) | |
| add(var_set(var_base.format('name'), quote(v.name))) | |
| add(var_set(var_base.format('platform'), quote(v.platform))) | |
| add(var_set(var_base.format('type'), quote(v.config_type))) | |
| add(var_set(var_base.format('charset'), quote(v.charset))) | |
| add(var_set(var_base.format('subsystem'), quote(v.subsystem))) | |
| add(var_set(var_base.format('output'), quote(v.out_name))) | |
| add(var_set(var_base.format("include_dirs"), None)) | |
| add(var_set(var_base.format("library_dirs"), None)) | |
| add(var_set(var_base.format("defines"), None)) | |
| add(var_set(var_base.format("libraries"), None)) | |
| add(list_set(var_base.format("include_dirs"), v.include_dirs)) | |
| add(list_set(var_base.format("library_dirs"), v.library_dirs)) | |
| add(list_set(var_base.format("defines"), v.defines)) | |
| add(list_set(var_base.format("libraries"), v.libraries)) | |
| add(var_set(var_base.format("custom_steps"), None)) | |
| add(var_set(var_base.format("unknown_tools"), None)) | |
| add(list_set(var_base.format("custom_steps"), v.custom_steps)) | |
| for step in v.custom_steps: | |
| cmd = v.custom_steps[step] | |
| add(var_set(var_base.format(step), quote(cmd, multiline=True))) | |
| add(list_set(var_base.format("unknown_tools"), v.unknown_tools)) | |
| cfg_idx += 1 | |
| num_groups = len(file_groups) | |
| add("# File 'filters'") | |
| add(var_set("num_groups", str(num_groups))) | |
| grp_idx = 0 | |
| for k in file_groups: | |
| var_base = 'flies_{}_{}'.format(grp_idx, '{}') | |
| add("# file group '{}'".format(k)) | |
| add(var_set(var_base.format("name"), quote(k))) | |
| var_list = variable(var_base.format("list")) | |
| add("set({}".format(var_list)) | |
| for v in file_groups[k]: | |
| if (v.have_configs): | |
| add('\t"{}" # custom build config'.format(v.file_path)) | |
| else: | |
| add('\t"{}"'.format(v.file_path)) | |
| pass | |
| add(") # {}".format(var_list)) | |
| grp_idx += 1 | |
| return '\n'.join(script) | |
| def save_cmake(path: str, script: str): | |
| try: | |
| with open(path, "w", encoding="UTF-8") as f: | |
| f.write(script) | |
| return 0 | |
| except Exception as e: | |
| print("File:", path) | |
| print("Exception while saving:", e.with_traceback()) | |
| return -1 | |
| def fix_vars(text:str) -> str: | |
| import re | |
| var_re = re.compile("(\$\(([\w]+)\))") | |
| return re.sub(var_re, r'${_\2}', text) # "$(Variable)" -> "${_Variable}" | |
| def main(argv: list[str]) -> int: | |
| parser = make_arg_parser(os.path.basename(argv[0])) | |
| params = parser.parse_args() | |
| if (params.input is None): | |
| parser.print_help() | |
| return -1 | |
| output = params.output if params.output else "{}.cmake".format(params.input) | |
| try: | |
| xml = load_vcprj(params.input, params.encoding) | |
| except Exception as e: | |
| print("File:", params.input) | |
| print("Exception while loading:", e) | |
| return -1 | |
| try: | |
| script = to_cmake(xml) | |
| except Exception as e: | |
| print("File:", params.input) | |
| print("Exception while parsing:", e) | |
| return -1 | |
| # fix variable format | |
| script = fix_vars(script) | |
| if (params.print): | |
| print(script) | |
| else: | |
| return save_cmake(output, script) | |
| return 0 | |
| if __name__ == "__main__": | |
| exit(main(sys.argv)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment