Skip to content

Instantly share code, notes, and snippets.

@max-dark
Last active October 21, 2024 14:18
Show Gist options
  • Save max-dark/c81cd849e1e2da544ae1366cb3925152 to your computer and use it in GitHub Desktop.
Save max-dark/c81cd849e1e2da544ae1366cb3925152 to your computer and use it in GitHub Desktop.
convert vc project to cmake
# 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}")
#!/usr/bin/env bash
/usr/bin/find . -name "*.vcproj" -exec python vc2cmake.py --input {} \;
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