Skip to content

Instantly share code, notes, and snippets.

@Andersama
Last active January 30, 2024 21:31
Show Gist options
  • Save Andersama/c28912d5454ce7651c1a7e2152a21473 to your computer and use it in GitHub Desktop.
Save Andersama/c28912d5454ce7651c1a7e2152a21473 to your computer and use it in GitHub Desktop.
# ------------------------------------------------------------------------
import N10X
import subprocess
import json
import re
import xml.etree.ElementTree as ET
import os
import operator
import hashlib
import copy
import platform
from os.path import exists
from os.path import split
from threading import Thread
import base64
"""
CMake build integration for 10x (10xeditor.com)
Version: 0.1.3
Original Script author: https://github.com/andersama
To get started go to Settings.10x_settings, and enable the hooks, by adding these lines:
CMake.HookBuild: true
CMake.HookWorkspaceOpened: true
This script assumes CMake.exe is in your path! To change this modify CMake.Path
CMake_Options:
- CMake.HookBuild: (default=False) Hooks CMake into build commands, detects cmake projects and executes OnCMakeBuildStarted
- CMake.HookWorkspaceOpened: (default=False) Hooks CMake into workspace opened commands, detects cmake project files inside the current workspace and writes 10x workspace files
- CMake.Path: Path to a custom cmake executable or directory, default assumes CMake is on the path!
- CMake.GuiPath: Path to cmake-gui executable or directory, default assumes cmake-gui is on the path!
- CMake.Verbose: (default=False) Turns on debugging print statements
History:
0.1.3
- Add processing of CMakeUserPresets.json and includes
0.1.2
- Add cmake_gui function (use with the command window) ctrl+shift+x
- Auto detect config and config / preset names for each workflow
0.1.1
- Fixed bugs and inheritance algorithm
0.1.0
- First Release
"""
# autopep8: off
def b64_encode(s):
return base64.b64encode(s.encode()).decode()
def read_json_file(json_path, verbose=False):
data = dict()
with open(json_path, "r", encoding="utf-8-sig") as json_data:
json_text = json_data.read()
if verbose:
print(json_text)
data = json.loads(json_text)
return data
def write_json_file(json_path, json_obj):
with open(json_path, "w") as f:
json.dump(json_obj, f)
def run_cmd(cmd_args, working_dir) -> str:
process = subprocess.Popen(
cmd_args,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
cwd=working_dir,
encoding="utf8",
)
returncode = process.wait()
result = process.stdout.read()
return result
# use \'s for paths, default window behavior
def norm_path_bslash(path) -> str:
return re.sub(r"[/]", "\\", os.path.normpath(path))
# use /'s for paths, allows copy/pasting into explorer (default is \)
def norm_path_fslash(path) -> str:
return re.sub(r"[\\]", "/", os.path.normpath(path))
def escape_bslash(input: str) -> str:
return re.sub("\\\\", "\\\\\\\\", input)
def macro_expansion(target_string: str, macros) -> str:
i = int(0)
# TODO: unescape strings
out_string = ""
while i < len(target_string):
em = i
if target_string[i] == "$": # potential macro expansion
while em < len(target_string) and target_string[em] != "}":
em += 1
macro_key = target_string[i : em + 1]
macro_value = macro_key
if macro_key in macros and macros[macro_key] != None:
macro_value = macros[macro_key]
out_string += macro_value
# jump past macro expansion
i = em + 1
else:
while em < len(target_string) and target_string[em] != "$":
em += 1
out_string += target_string[i:em]
# jump past substring
i = em
return out_string
def macro_expand_any(item, macros):
if type(item) is str:
return macro_expansion(item, macros)
elif type(item) is dict:
for key in item:
item[key] = macro_expand_any(item[key], macros)
return item
elif type(item) is list:
for i in range(0, len(item)):
item[i] = macro_expand_any(item[i], macros)
return item
# otherwise just return the item as is
return item
def cmake_inheirit(child_obj, parent_obj, top_level=True):
expanded_obj = copy.deepcopy(parent_obj)
# see: https://cmake.org/cmake/help/git-master/manual/cmake-presets.7.html#configure-preset
if top_level: # don't inheirit these
expanded_obj.pop("name", None)
expanded_obj.pop("description", None)
expanded_obj.pop("displayName", None)
expanded_obj.pop("hidden", None)
for key in child_obj:
if key in expanded_obj and type(child_obj[key]) == type(expanded_obj[key]):
if type(child_obj[key]) is dict:
expanded_obj[key] = cmake_inheirit(
child_obj[key], expanded_obj[key], False
)
elif type(child_obj[key]) is list:
# inherit chain*
expanded_obj[key].extend(child_obj[key])
else:
expanded_obj[key] = child_obj[key]
else:
expanded_obj[key] = child_obj[key]
if top_level: # we've successfully inherited the previous, remove this item
expanded_obj.pop("inherits", None)
return expanded_obj
def cmake_merge_userdata(child_data: dict, key: str, parent_data: dict = {}):
configs = []
if key in parent_data: # we're expecting a to be a list!
configs = copy.deepcopy(parent_data[key])
parent_len = len(configs)
# add child configs
if key in child_data:
configs.extend(child_data[key])
depths = []
# first passes to figure out inheiritance of configs
# it's an error for any config object to have matching "name" keys, user data doesn't appear to "override" or "overwrite"
seen = {}
dup = []
err_msg = ""
dup_err = False
for config in configs:
if config["name"] not in seen:
seen[config["name"]] = config
else:
dup_err = True
dup.append(config)
if dup_err:
err_msg = "Error!: Duplicate presets\n"
for config in dup:
err_msg += json.dumps(config, indent="\t")
err_msg += "\n"
return {"configs": configs, "error": dup_err, "error_message": err_msg}
def cmake_inherit_algorithm(configs: list, parent_len):
depths = []
# first passes to figure out inheiritance of configs
# first let the parent configs inherit from themselves
for i in range(0, parent_len): # config in configs:
config = configs[i]
if "inherits" in config:
new_config = dict()
inherits_list = []
if type("inherits") is list:
inherits_list = config["inherits"]
else: # should be a string
inherits_list.append(config["inherits"])
child_name = config["name"]
for idx in range(0, len(inherits_list)):
parent_name = inherits_list[idx]
for j in range(0, parent_len): # len(configs)
c = configs[j]
if c["name"] == parent_name:
if "inherits" in c:
depths.append(
{
"config": child_name,
"parent": parent_name,
"depth": 2,
"self_index": i,
"parent_index": j,
}
)
else:
depths.append(
{
"config": child_name,
"parent": parent_name,
"depth": 1,
"self_index": i,
"parent_index": j,
}
)
break
else:
depths.append(
{
"config": config["name"],
"parent": None,
"depth": 0,
"self_index": i,
"parent_index": 0,
}
)
# second let the child configs inherit from both the parents and themselves
for i in range(parent_len, len(configs)):
config = configs[i]
if "inherits" in config:
new_config = dict()
inherits_list = []
if type("inherits") is list:
inherits_list = config["inherits"]
else: # should be a string
inherits_list.append(config["inherits"])
child_name = config["name"]
for idx in range(0, len(configs)):
parent_name = inherits_list[idx]
for j in range(0, parent_len): # len(configs)
c = configs[j]
if c["name"] == parent_name:
if "inherits" in c:
depths.append(
{
"config": child_name,
"parent": parent_name,
"depth": 2,
"self_index": i,
"parent_index": j,
}
)
else:
depths.append(
{
"config": child_name,
"parent": parent_name,
"depth": 1,
"self_index": i,
"parent_index": j,
}
)
break
else:
depths.append(
{
"config": config["name"],
"parent": None,
"depth": 0,
"self_index": i,
"parent_index": 0,
}
)
start_index = 0 # this counts the # of items with 0 depth
while True:
mx_depth = 0
# this is to recurse through inheirited configs in a linear fashion
start_loop = True # len(depths)-start_index, 0, -1
for i in reversed(
range(0, len(depths) - start_index)
): # reverse the loop to handle precedence of inheiritance (earliest > precedence)
item = depths[i]
if item["depth"] == 0 and start_loop:
start_index = (
start_index + 1
) # continue the loop past this point in future
continue
child_index = item["self_index"]
parent_index = item["parent_index"]
depth = depths[parent_index]["depth"] + 1
if (item["parent"] != None and depth == 1) or item["depth"] == 1:
if i + 1 < len(depths):
prev_item = depths[i - 1]
prev_child_index = prev_item["self_index"]
prev_parent_index = prev_item["parent_index"]
prev_depth = prev_item["depth"]
child_index = item["self_index"]
parent_index = item["parent_index"]
if child_index == prev_child_index and prev_depth > 0:
# haven't finished inheriting something which comes first, wait to inherit
start_loop = False
else:
# note: we only inheirit from parents w/ no parents*
configs[child_index] = cmake_inheirit(
configs[child_index], configs[parent_index]
)
configs[child_index].pop("inherits", None)
depths[child_index]["parent"] = None
depths[child_index]["depth"] = 0
if start_loop:
start_index = (
start_index + 1
) # continue the loop past this point in future
else:
# note: we only inheirit from parents w/ no parents*
configs[child_index] = cmake_inheirit(
configs[child_index], configs[parent_index]
)
configs[child_index].pop("inherits", None)
depths[child_index]["parent"] = None
depths[child_index]["depth"] = 0
if start_loop:
start_index = (
start_index + 1
) # continue the loop past this point in future
else:
configs[child_index][
"depth"
] = depth # update this item's current depth (doesn't matter)
start_loop = False
if depths[i]["depth"] > mx_depth:
mx_depth = depths[i]["depth"]
if not (mx_depth > 0):
break # finally we're done, everything has inheirited its parent properly
# data[key] = configs
return configs
def cmake_merge_item(
preset_data: dict, user_data: dict, args: dict, macros: dict, key: str
):
data = dict()
data[key] = []
data["error"] = False
data["error_message"] = ""
if key in preset_data or key in user_data:
merged = cmake_merge_userdata(preset_data, key, user_data) # user_data,key,data
parent_len = 0
if key in preset_data:
parent_len = len(preset_data[key])
if merged["error"]:
data[key] = merged["configs"]
data["error"] = merged["error"]
data["error_message"] = merged["error_message"]
return data
configs = cmake_inherit_algorithm(merged["configs"], parent_len)
# unexpanded_configs = configs
for i in range(0, len(configs)):
if "generator" in configs[i]:
macros["${generator}"] = macro_expand_any(
configs[i]["generator"], macros
)
else:
macros["${generator}"] = None
if "name" in configs[i]:
macros["${presetName}"] = macro_expand_any(configs[i]["name"], macros)
macros["${name}"] = macros["${presetName}"]
else:
macros["${presetName}"] = None
macros["${name}"] = None
configs[i] = macro_expand_any(configs[i], macros)
# add in the cmake variables over commandline (overrides)
for cmake_var in args["entries"]: # macro expand the variables?
k = macro_expand_any(cmake_var["name"], macros)
v = macro_expand_any(cmake_var["value"], macros)
configs[i]["cacheVariables"][k] = v
data[key] = configs
return data
def cmake_parse_args(cmd_args: list) -> dict:
# see: https://cmake.org/cmake/help/latest/manual/cmake.1.html
# -D <var>:<type>=<value>, -D <var>=<value>
# -C <intial-cache>
# -G <generator-name>
# returns a cache-v2 json like dictionary of cmake variables that got parsed over command-line, later entries have greater precedence
cmake_cache = dict()
cmake_cache["entries"] = []
insert_point = 0
skip_arg = False
for i in range(0, len(cmd_args)):
arg = cmd_args[i]
if skip_arg:
skip_arg = False
continue
if arg == "cmake":
continue # presumably the start of the command line if someone forgot to strip the exe
if arg.startswith("-D"):
m = re.match("^-D([^:=]*)(:[^=]*)?=(.*)$")
cmake_cache["entries"].append(
{"name": m.group(1), "type": m.group(2), "value": m.group(3)}
)
elif arg == "-G" and i + 1 < len(cmd_args):
cmake_cache["entries"].append(
{
"name": "CMAKE_GENERATOR",
"properties": [
{"name": "HELPSTRING", "value": "Name of generator."}
],
"type": "INTERNAL",
"value": cmd_args[i + 1],
}
)
skip_arg = True
elif arg == "-C" and i + 1 < len(cmd_args):
cached = read_json_file(cmd_args[i + 1])
if "entries" in cached and type(cached["entries"]) is list:
count = len(cached["entries"])
cmake_cache["entries"][insert_point:insert_point] = cached["entries"]
insert_point += count
skip_arg = True
elif arg == "--config" and i + 1 < len(cmd_args):
cmake_cache["entries"].append(
{
"name": "CMAKE_CONFIG_NAME",
"type": "INTERNAL",
"value": cmd_args[i + 1],
}
)
skip_arg = True
elif arg.startswith("--preset"):
# --preset <preset>, --preset=<preset>
if arg == "--preset" and i + 1 < len(cmd_args):
preset_name = cmd_args[i + 1]
cmake_cache["entries"].append(
{
"name": "CMAKE_PRESET_NAME",
"type": "INTERNAL",
"value": preset_name,
}
)
skip_arg = True
else:
m = re.match("^--preset=(.*)$")
cmake_cache["entries"].append(
{
"name": "CMAKE_PRESET_NAME",
"type": "INTERNAL",
"value": m.group(1),
}
)
elif arg == "-A" and i + 1 < len(cmd_args):
# -A <platform-name> Specify platform name if supported by generator.
cmake_cache["entries"].append(
{
"name": "CMAKE_GENERATOR_PLATFORM",
"properties": [
{"name": "HELPSTRING", "value": "Name of generator platform."}
],
"type": "INTERNAL",
"value": cmd_args[i + 1],
}
)
skip_arg = True
elif arg == "-S" and i + 1 < len(cmd_args):
cmake_cache["entries"].append(
{
"name": "CMAKE_CURRENT_PROJECT_SOURCE_DIR",
"properties": [
{"name": "HELPSTRING", "value": "Value Computed by 10xEditor"}
],
"type": "STATIC",
"value": cmd_args[i + 1],
}
)
skip_arg = True
elif arg == "-B" and i + 1 < len(cmd_args):
cmake_cache["entries"].append(
{
"name": "CMAKE_CURRENT_PROJECT_BINARY_DIR",
"properties": [
{"name": "HELPSTRING", "value": "Value Computed by 10xEditor"}
],
"type": "STATIC",
"value": cmd_args[i + 1],
}
)
skip_arg = True
elif arg == "--install-prefix" and i + 1 < len(cmd_args):
cmake_cache["entries"].append(
{
"name": "CMAKE_INSTALL_PREFIX",
"properties": [
{
"name": "HELPSTRING",
"value": "Specifies the installation directory. Must be an absolute path.",
}
],
"type": "PATH",
"value": cmd_args[i + 1],
}
)
skip_arg = True
# TODO: parse more command line and make it available to python
return cmake_cache
# see: https://cmake.org/cmake/help/latest/manual/cmake-presets.7.html#macro-expansion
def cmake_prep(
source_dir,
build_dir,
cmd_args,
use_presets_if_available=True,
use_settings_if_available=True,
):
cmakelists_exists = IsCMakeDirectory(source_dir)
if not (cmakelists_exists):
return dict()
presetpath = norm_path_fslash(os.path.join(source_dir, "CMakePresets.json"))
userpresetpath = presetpath[: -len("CMakePresets.json")] + "CMakeUserPresets.json"
settingspath = presetpath[: -len("CMakePresets.json")] + "CMakeSettings.json"
listspath = presetpath[: -len("CMakePresets.json")] + "CMakeLists.txt"
preset_exists = exists(presetpath)
user_preset_exists = exists(userpresetpath)
settings_exists = exists(settingspath)
args = cmake_parse_args(cmd_args)
projectFile = listspath
if settings_exists:
thisFile = settingspath
thisFileDir, thisFileName = split(thisFile)
else:
thisFile = (None,)
thisFileDir = None
# command line overrides
generator = None
bin_dir = build_dir
src_dir = source_dir
config_name = None
preset_name = None
for cmake_var in args["entries"]:
if cmake_var["name"] == "CMAKE_GENERATOR":
generator = cmake_var["value"]
elif cmake_var["name"] == "CMAKE_CURRENT_PROJECT_BINARY_DIR":
bin_dir = cmake_var["value"]
elif cmake_var["name"] == "CMAKE_CURRENT_PROJECT_SOURCE_DIR":
src_dir = cmake_var["value"]
elif cmake_var["name"] == "CMAKE_CONFIG_NAME":
config_name = cmake_var["value"]
elif cmake_var["name"] == "CMAKE_PRESET_NAME":
preset_name = cmake_var["value"]
src_parent, src_name = split(src_dir)
bin_parent = None
bin_name = None
if bin_dir:
bin_parent, bin_name = split(bin_dir)
hasher = hashlib.shake_256(bytes(src_dir, encoding="utf-8-sig"))
workspaceHash = hasher.hexdigest(20)
macros = {
"${sourceDir}": src_dir,
"${sourceParentDir}": src_parent,
"${sourceDirName}": src_name,
"${presetName}": preset_name, # like --Config
"${generator}": generator,
"${hostSystemName}": platform.system(),
"${dollar}": "$",
"${pathListSep}": ";", # TODO: this apparently changes given an OS? #a native character for separating lists of paths
# old settings.json macros
"${workspaceRoot}": src_dir,
"${workspaceHash}": str(
workspaceHash
), # TODO: find out what hash functions typically get used here
"${projectFile}": projectFile,
"${projectDir}": src_dir,
"${thisFile}": thisFile,
"${thisFileDir}": thisFileDir,
"${name}": config_name, # like --preset
}
# see: https://learn.microsoft.com/en-us/cpp/build/cmake-presets-json-reference?view=msvc-170
# $env{<variable-name>} environment variable with name <variable-name>
# $penv{<variable-name>} Similar to $env{<variable-name>}, except that the value only comes from the parent environment, and never
# $vendor{<macro-name>} An extension point for vendors to insert their own macros. CMake will not be able to use presets which have a $vendor{<macro-name>} macro, and effectively ignores such presets. However, it will still be able to use other presets from the same file.
# print(f'{key}: {value}')
# see: https://learn.microsoft.com/en-us/cpp/build/cmakesettings-reference?view=msvc-170#macros
# ${macro}
# ${workspaceRoot}, ${workspaceHash}, ${projectFile}, ${projectDir}, ${thisFile}, ${thisFileDir}, ${name}, ${generator}, ${env.VARIABLE}
# ${env.<VARIABLE>} environment variable with name <variable-name>
for key, value in os.environ.items():
macros["$env{" + key + "}"] = value
macros["${env." + key + "}"] = value
if cmakelists_exists:
if use_presets_if_available and (preset_exists or user_preset_exists):
# preset_data = dict()
# if preset_exists:
# preset_data = read_json_file(presetpath)
#
# user_data = dict()
# if user_preset_exists:
# user_data = read_json_file(userpresetpath)
# norm_path_fslash
includes = []
include_objs = []
# normpath(join(os.getcwd(), path))
if user_preset_exists:
abs_preset_path = norm_path_fslash(
os.path.normpath(os.path.join(source_dir, userpresetpath))
)
includes.append(abs_preset_path)
if preset_exists: # user presets implicitly include presets by default
abs_preset_path = norm_path_fslash(
os.path.normpath(os.path.join(source_dir, presetpath))
)
if not abs_preset_path in includes:
includes.append(presetpath)
i = 0
while i < len(includes):
preset_obj = read_json_file(includes[i])
include_objs.append(preset_obj)
if "include" in preset_obj:
current_includes = []
# construct an array to refer to all the loaded json objects later
preset_obj["include_idxs"] = []
if type(preset_obj["include"]) is str:
current_includes.append(
norm_path_fslash(
os.path.normpath(
os.path.join(source_dir, preset_obj["include"])
)
)
)
elif type(preset_obj["include"]) is list:
for include in preset_obj["include"]:
current_includes.append(
norm_path_fslash(
os.path.normpath(os.path.join(source_dir, include))
)
)
for include in current_includes:
already_loaded = False
for idx in range(0, len(includes)):
if includes[idx] == include:
preset_obj["include_idxs"].append(idx)
already_loaded = True
break
if (
not already_loaded
): # this might be an error because this could mean a garunteed overlap?
preset_obj["include_idxs"].append(len(includes))
includes.append(include)
# next in queue
i = i + 1
data = dict()
data["configurePresets"] = []
data["buildPresets"] = []
data["testPresets"] = []
data["packagePresets"] = []
data["workflowPresets"] = []
# if "configurePresets" in preset_data or "configurePresets" in user_data:
for i in range(0, len(include_objs)):
if i == 0 and user_preset_exists:
continue
if "configurePresets" in include_objs[i]:
data["configurePresets"].extend(include_objs[i]["configurePresets"])
user_data = {}
if user_preset_exists and "configurePresets" in include_objs[0]:
user_data = include_objs[0]
merged = cmake_merge_item(data, user_data, args, macros, "configurePresets")
data["configurePresets"] = merged["configurePresets"]
data["configure_error"] = merged["error"]
data["configure_error_message"] = merged["error_message"]
# if "buildPresets" in data or "buildPresets" in user_data:
for i in range(0, len(include_objs)):
if i == 0 and user_preset_exists:
continue
if "buildPresets" in include_objs[i]:
data["buildPresets"].extend(include_objs[i]["buildPresets"])
user_data = {}
if user_preset_exists and "buildPresets" in include_objs[0]:
user_data = include_objs[0]
merged = cmake_merge_item(data, user_data, args, macros, "buildPresets")
data["buildPresets"] = merged["buildPresets"]
data["build_error"] = merged["error"]
data["build_error_message"] = merged["error_message"]
# if "testPresets" in data or "testPresets" in user_data:
for i in range(0, len(include_objs)):
if i == 0 and user_preset_exists:
continue
if "testPresets" in include_objs[i]:
data["testPresets"].extend(include_objs[i]["testPresets"])
user_data = {}
if user_preset_exists and "testPresets" in include_objs[0]:
user_data = include_objs[0]
merged = cmake_merge_item(data, user_data, args, macros, "testPresets")
data["testPresets"] = merged["testPresets"]
data["test_error"] = merged["error"]
data["test_error_message"] = merged["error_message"]
# if "packagePresets" in data or "packagePresets" in user_data:
for i in range(0, len(include_objs)):
if i == 0 and user_preset_exists:
continue
if "packagePresets" in include_objs[i]:
data["packagePresets"].extend(include_objs[i]["packagePresets"])
user_data = {}
if user_preset_exists and "packagePresets" in include_objs[0]:
user_data = include_objs[0]
merged = cmake_merge_item(data, user_data, args, macros, "packagePresets")
data["packagePresets"] = merged["packagePresets"]
data["package_error"] = merged["error"]
data["package_error_message"] = merged["error_message"]
# if "workflowPresets" in data or "workflowPresets" in user_data:
for i in range(0, len(include_objs)):
if i == 0 and user_preset_exists:
continue
if "workflowPresets" in include_objs[i]:
data["workflowPresets"].extend(include_objs[i]["workflowPresets"])
user_data = {}
if user_preset_exists and "workflowPresets" in include_objs[0]:
user_data = include_objs[0]
merged = cmake_merge_item(data, user_data, args, macros, "workflowPresets")
data["workflowPresets"] = merged["workflowPresets"]
data["workflow_error"] = merged["error"]
data["workflow_error_message"] = merged["error_message"]
data["macros"] = macros
data["error"] = (
("configure_error" in data and data["configure_error"])
or ("build_error" in data and data["build_error"])
or ("test_error" in data and data["test_error"])
or ("package_error" in data and data["package_error"])
or ("workflow_error" in data and data["workflow_error"])
)
if "entries" in args:
data["entries"] = args["entries"]
return data
if use_settings_if_available and len(settingspath) > 0:
data = read_json_file(settingspath)
data["macros"] = macros
# settings don't do inheritance? but they definitely expand macros
unexpanded_configs = data["configurations"]
for i in range(0, len(unexpanded_configs)):
if "generator" in unexpanded_configs[i]:
macros["${generator}"] = macro_expand_any(
unexpanded_configs[i]["generator"], macros
)
else:
macros["${generator}"] = (
generator # will leave ${generator} unexpanded
)
if "name" in unexpanded_configs[i]:
macros["${presetName}"] = macro_expand_any(
unexpanded_configs[i]["name"], macros
)
macros["${name}"] = macros["${presetName}"]
else:
macros["${presetName}"] = None
macros["${name}"] = None
for cmake_var in args["entries"]:
found = False
found_idx = 0
for v in range(0, len(data["configurations"][i]["variables"])):
config_var = data["configurations"][i]["variables"][v]
if config_var["name"] == cmake_var["name"]:
found = True
found_idx = v
break
cmake_var_copy = copy.deepcopy(cmake_var)
cmake_var_copy.pop("properties", None)
if found:
data["configurations"][i]["variables"][
found_idx
] = cmake_var_copy
else:
data["configurations"][i]["variables"].append(cmake_var_copy)
data["configurations"][i] = macro_expand_any(
unexpanded_configs[i], macros
)
data = macro_expand_any(data, macros)
if "entries" in args:
data["entries"] = args["entries"]
return data
# Handling of empty project
data = {}
if "entries" in args:
data["entries"] = args["entries"]
data["macros"] = macros
return data
# failover use just cmake, no presets.json, no settings.json
return dict()
def cmake_version():
CMake_EXE = N10X.Editor.GetSetting("CMake.Path").strip()
if not CMake_EXE:
CMake_EXE = "cmake"
if os.path.isdir(CMake_EXE):
CMake_EXE = os.path.join(CMake_EXE, "cmake.exe")
txworkspace = N10X.Editor.GetWorkspaceFilename()
directory, filename = split(txworkspace) # os.path.split
stdtxt = run_cmd([CMake_EXE, "--version"], directory)
r = re.search("^cmake version (\\d+).(\\d+).(\\d+)[-](\\w+)?", stdtxt)
if r != None:
result = {
"major": int(r.group(1)),
"minor": int(r.group(2)),
"patch": int(r.group(3)),
"tag": r.group(4),
}
result["preset_support"] = result["major"] >= 3 and result["minor"] >= 1
else:
result = {
"major": 0,
"minor": 0,
"patch": 0,
"tag": "",
"preset_support": False,
}
print(result)
return result
def cmake_gui(src_dir: str = ""):
txworkspace = N10X.Editor.GetWorkspaceFilename()
directory, filename = split(txworkspace) # os.path.split
if len(src_dir) == 0 or src_dir == ".":
src_dir = directory
cmakelists_exists = IsCMakeDirectory(src_dir)
cmake_gui = N10X.Editor.GetSetting("CMake.GuiPath").strip()
if not cmake_gui:
cmake_gui = "cmake-gui"
if os.path.isdir(cmake_gui):
cmake_gui = os.path.join(cmake_gui, "cmake-gui.exe")
if cmakelists_exists:
stdtxt = run_cmd([cmake_gui, "-S", src_dir], src_dir)
return stdtxt
return ""
def cmake_configure(cmd_args, working_dir) -> dict:
# ["cmake", ...]
stdtxt = run_cmd(cmd_args, working_dir)
# read the stdout to grab the build directory
N10X.Editor.LogToBuildOutput(stdtxt)
# debugging
# all we technically need as feedback is the build directory we can extract from
# the command line later if needed so I'm writing a .json file behind
result = {"stdout": stdtxt, "build_dir": None}
r = re.search("-- Build files have been written to:\\s+(.+)$", stdtxt)
if r != None:
result["build_dir"] = r.group(1)
return result
def cmake_build(cmd_args, working_dir, build_dir) -> dict:
# ["cmake", ...]
# write file api query
query_dir = norm_path_fslash(
os.path.join(build_dir, ".cmake/api/v1/query/client-10xeditor")
)
reply_dir = norm_path_fslash(os.path.join(build_dir, ".cmake/api/v1/reply/"))
query_file = norm_path_fslash(os.path.join(query_dir, "query.json"))
query_json = {
"requests": [
{"kind": "cache", "version": 2},
{"kind": "cmakeFiles", "version": 1},
{"kind": "codemodel", "version": 2},
{"kind": "toolchains", "version": 1},
]
}
# ensure the folder exists before we attempt to write to it
if not os.path.exists(query_dir):
try:
os.makedirs(query_dir)
except OSError as e:
if e.errno != errno.EEXIST:
raise
with open(query_file, "w") as f:
json.dump(query_json, f)
stdtxt = run_cmd(cmd_args, working_dir)
N10X.Editor.LogToBuildOutput(stdtxt)
result = {"stdout": stdtxt, "exe": None, "sln": None, "pdb": None}
index_json = None
# read file api reply
for root, dirs, files in os.walk(reply_dir):
for file in files:
# find the index file
if re.match("^(index-.+[.]json)$", file):
index_json = file
break
if index_json == None or len(index_json) <= 0:
print(
"No index.json file found in reply directory, make sure you're running an up to date version of CMake"
)
return result
index_path = norm_path_fslash(os.path.join(reply_dir, index_json))
index_data = read_json_file(index_path)
generator_data = index_data["cmake"]["generator"]
generator = generator_data["name"]
platform = generator_data["platform"]
# Read "objects" which includes all the json file responses
objects_data = index_data["objects"]
cache_json_obj = dict()
codemodel_json_obj = dict()
cmakefiles_json_obj = dict()
toolchains_json_obj = dict()
for obj in objects_data:
kind = obj["kind"]
if kind.startswith("cache"):
cache_json_obj = obj
elif kind.startswith("codemodel"):
codemodel_json_obj = obj
elif kind.startswith("cmakeFiles"):
cmakefile_json_obj = obj
elif kind.startswith("toolchains"):
toolchains_json_obj = obj
potential_targets = []
if len(codemodel_json_obj["jsonFile"]) > 0:
codemodel_path = norm_path_fslash(
os.path.join(reply_dir, codemodel_json_obj["jsonFile"])
)
codemodel_data = read_json_file(codemodel_path)
source_dir = codemodel_data["paths"]["source"]
configuration_data = codemodel_data["configurations"]
for obj in configuration_data:
config_name = obj["name"] # Debug, Release, RelWithDebInfo, MinSizeRel
targets_data = obj["targets"]
main_project_data = obj["projects"][0]
project_name = main_project_data["name"]
target_obj = dict()
for target in targets_data:
if target["name"] == project_name:
target_obj = target
potential_targets.append(target)
break
# find a target which has an .exe path which exists and matches
# the internal project_name
# assume the most recent is the actual build
mtime = 0
newest_target = None
newest_pdb = None
for target in potential_targets:
target_path = norm_path_fslash(os.path.join(reply_dir, target["jsonFile"]))
target_data = read_json_file(target_path)
artifacts = target_data["artifacts"]
executable_obj = artifacts[0]
pdb_obj = None
if len(artifacts) > 1:
pdb_obj = artifacts[1]
exe = norm_path_fslash(os.path.join(build_dir, executable_obj["path"]))
if pdb_obj != None:
pdb = norm_path_fslash(os.path.join(build_dir, pdb_obj["path"]))
if exists(exe):
exe_mtime = os.path.getmtime(exe)
if exe_mtime > mtime:
mtime = exe_mtime
newest_target = exe
newest_pdb = pdb
project_exe = newest_target
if newest_target and len(newest_target) > 0:
N10X.Editor.SetWorkspaceSetting("RunCommand", project_exe)
N10X.Editor.SetWorkspaceSetting("DebugCommand", project_exe)
N10X.Editor.SetWorkspaceSetting("ExePath", project_exe)
else:
N10X.Editor.SetWorkspaceSetting("RunCommand", "")
N10X.Editor.SetWorkspaceSetting("DebugCommand", "")
N10X.Editor.SetWorkspaceSetting("ExePath", "")
# find an sln if there is one, use the most recently changed
newest_sln = None
mtime_sln = 0
for root, dirs, files in os.walk(build_dir):
for file in files:
if file.endswith(".sln"):
sln_path = norm_path_fslash(os.path.join(root, file))
file_mtime = os.path.getmtime(sln_path)
if file_mtime > mtime_sln:
mtime_sln = file_mtime
newest_sln = sln_path
N10X.Editor.SetWorkspaceSetting("DebugSln", newest_sln)
result["exe"] = newest_target
result["sln"] = newest_sln
result["pdb"] = newest_pdb
return newest_target
def write10xWorkspace(
outpath,
build_cmd,
rebuild_cmd,
buildfile_cmd,
clean_cmd,
buildworkingdirectory_cmd,
cancelbuild_cmd,
runcommand_cmd,
runcommandworkingdirectory_cmd,
debug_cmd,
exepath,
debugsln,
configlist=[],
platformlist=[],
):
root = ET.Element("N10X")
doc = ET.SubElement(root, "Workspace")
ET.SubElement(doc, "IncludeFilter").text = "*.*"
ET.SubElement(doc, "ExcludeFilter").text = (
"*.obj,*.lib,*.pch,*.dll,*.pdb,.vs,Debug,Release,x64,obj,*.user,Intermediate,*.vcxproj,*.vcxproj.filters"
)
ET.SubElement(doc, "SyncFiles").text = "true"
ET.SubElement(doc, "Recursive").text = "true"
ET.SubElement(doc, "ShowEmptyFolders").text = "true"
ET.SubElement(doc, "IsVirtual").text = "false"
ET.SubElement(doc, "IsFolder").text = "false"
ET.SubElement(doc, "BuildCommand").text = build_cmd
ET.SubElement(doc, "RebuildCommand").text = rebuild_cmd
ET.SubElement(doc, "BuildFileCommand").text = buildfile_cmd
ET.SubElement(doc, "CleanCommand").text = clean_cmd
ET.SubElement(doc, "BuildWorkingDirectory").text = buildworkingdirectory_cmd
ET.SubElement(doc, "CancelBuild").text = cancelbuild_cmd
ET.SubElement(doc, "RunCommand").text = runcommand_cmd
ET.SubElement(doc, "RunCommandWorkingDirectory").text = (
runcommandworkingdirectory_cmd
)
ET.SubElement(doc, "DebugCommand").text = debug_cmd
ET.SubElement(doc, "ExePathCommand").text = exepath
ET.SubElement(doc, "DebugSln").text = debugsln
ET.SubElement(doc, "UseVisualStudioEnvBat").text = "true"
# A bit of extra XML
ET.SubElement(doc, "UseCMake").text = "true"
config_element = ET.SubElement(doc, "Configurations")
for config in configlist:
ET.SubElement(config_element, "Configuration").text = config
platform_element = ET.SubElement(doc, "Platforms")
for platform in platformlist:
ET.SubElement(platform_element, "Platform").text = platform
additional_include_paths = ET.SubElement(doc, "AdditionalIncludePaths")
ET.SubElement(additional_include_paths, "AdditionalIncludePath")
ET.SubElement(doc, "Defines")
# TODO: pretty print
tree = ET.ElementTree(root)
xml_text = ET.tostring(root, xml_declaration=True).decode()
f = None
try:
f = open(outpath, "x")
except IOError:
print(outpath + " already exists")
else:
print("Writing CMake Workspace: " + outpath)
f.write(xml_text)
finally:
if f:
f.close()
def IsCMakeDirectory(directory) -> bool:
return exists(norm_path_fslash(os.path.join(directory, "CMakeLists.txt")))
def IsCMakeCacheDirectory(directory) -> bool:
return exists(norm_path_fslash(os.path.join(directory, "CMakeCache.txt")))
def IsCMakePresetDirectory(directory) -> bool:
return exists(norm_path_fslash(os.path.join(directory, "CMakePresets.json")))
def IsCMakeSettingsDirectory(directory) -> bool:
return exists(norm_path_fslash(os.path.join(directory, "CMakeSettings.json")))
def CMakeProjectName(cache_directory) -> str:
project_name = None
if len(cache_directory):
process = subprocess.Popen(
[
"findstr",
"CMAKE_PROJECT_NAME",
norm_path_fslash(os.path.join(cache_directory, "CMakeCache.txt")),
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
# cwd=cmakefolder,
encoding="utf8",
)
returncode = process2.wait()
result = process2.stdout.read()
r = re.search(r"=(.*)$", result)
if r != None:
project_name = r.group(1)
return project_name
def CMakeBuildThreaded(args: dict):
print(json.dumps(args, indent="\t"))
preset_exists = args["preset_exists"]
user_preset_exists = args["user_preset_exists"]
settings_exists = args["settings_exists"]
directory = args["directory"]
version = args["version"]
n10x_config = args["config"] # N10X.Editor.GetWorkspaceBuildConfig()
n10x_platform = args["platform"] # N10X.Editor.GetWorkspaceBuildPlatform()
verbose = args["verbose"]
CMake_EXE = args["cmake"]
rebuild = args["rebuild"]
print("CMake Build Detected: Using Python")
build_name = n10x_platform + "-" + n10x_config
N10X.Editor.LogToBuildOutput(
"----- Build {} {} -----\n".format(n10x_config, n10x_platform)
)
if (preset_exists or user_preset_exists) and version["preset_support"]:
print("CMake Macro Expansion: ...")
# TODO detect and parse command line to pass into this function
data = cmake_prep(directory, None, [], True, False)
if "error" in data and data["error"]:
if "error_message" in data:
N10X.Editor.LogToBuildOutput("{0}\n".format(data["error_message"]))
if "configure_error_message" in data:
N10X.Editor.LogToBuildOutput(
"{0}\n".format(data["configure_error_message"])
)
if "build_error_message" in data:
N10X.Editor.LogToBuildOutput(
"{0}\n".format(data["build_error_message"])
)
if "test_error_message" in data:
N10X.Editor.LogToBuildOutput("{0}\n".format(data["test_error_message"]))
if "package_error_message" in data:
N10X.Editor.LogToBuildOutput(
"{0}\n".format(data["package_error_message"])
)
if "workflow_error_message" in data:
N10X.Editor.LogToBuildOutput(
"{0}\n".format(data["workflow_error_message"])
)
N10X.Editor.LogToBuildOutput("----- CMake Build Failed -----\n")
N10X.Editor.OnBuildFinished(False)
return True
builds = []
configs = []
if "buildPresets" in data:
builds = data["buildPresets"]
if "configurePresets" in data:
configs = data["configurePresets"]
if type(builds) != list:
N10X.Editor.OnBuildFinished(False)
return True
# easy build, we'll automatically pick the configurePreset name to go with a buildPreset
cmake_config_name = None
cmake_build_name = None
cmake_config_obj = {}
for build in builds:
if build["name"] == build_name:
cmake_build_name = build["name"]
cmake_config_name = build["configurePreset"]
break
# failed to find a matching $(Configuration)-$(Platform) build now look for just $(Configuration)
if cmake_build_name == None:
for build in builds:
if build["name"] == n10x_config:
cmake_build_name = build["name"]
cmake_config_name = build["configurePreset"]
break
# try to find case insensitive versions as fallback
if cmake_build_name == None:
low_build_name = build_name.lower()
for build in builds:
if build["name"].lower() == low_build_name:
cmake_build_name = build["name"]
cmake_config_name = build["configurePreset"]
break
if cmake_build_name == None:
low_n10x_config = n10x_config.lower()
for build in builds:
if build["name"].lower() == low_n10x_config:
cmake_build_name = build["name"]
cmake_config_name = build["configurePreset"]
break
if cmake_config_name == None:
for config in configs:
if config["name"] == build_name:
cmake_config_name = config["name"]
cmake_config_obj = config
break
if cmake_config_name == None:
for config in configs:
if config["name"] == n10x_config:
cmake_config_name = config["name"]
cmake_config_obj = config
break
# try to find case insensitive versions as fallback
if cmake_config_name == None:
low_build_name = build_name.lower()
for config in configs:
if config["name"].lower() == low_build_name:
cmake_config_name = config["name"]
cmake_config_obj = config
break
if cmake_config_name == None:
low_n10x_config = n10x_config.lower()
for config in configs:
if config["name"].lower() == low_n10x_config:
cmake_config_name = config["name"]
cmake_config_obj = config
break
else: # found a matching buildPreset, now find the configPreset
for config in configs:
if config["name"] == cmake_config_name:
cmake_config_obj = config
break
# if we don't find a match here, strictly speaking this is a badly configured CMakePresets.json
# TODO? Nearest config/preset name (did you mean) feedback?
if cmake_config_name == None:
N10X.Editor.LogToBuildOutput(
"Failed to find a build or config preset in CMakePresets.json for: {0} and {1}\n".format(
build_name, n10x_config
)
)
N10X.Editor.LogToBuildOutput("----- CMake Build Failed -----\n")
N10X.Editor.OnBuildFinished(False)
return True
cmake_macros = {}
if "macros" in data:
cmake_macros = data["macros"]
cmake_macros["${presetName}"] = cmake_config_name
# update the config and update by expanding presetName
cmake_config_obj = macro_expand_any(cmake_config_obj, cmake_macros)
if verbose:
print(json.dumps(cmake_config_obj, indent="\t"))
# build_directory_path = norm_path_fslash(os.path.join(directory, "out\\build", cmake_build_name))
build_directory_path = None
if "binaryDir" in cmake_config_obj:
build_directory_path = cmake_config_obj["binaryDir"]
elif cmake_config_name != None:
build_directory_path = norm_path_fslash(
os.path.join(directory, "out\\build", cmake_config_name)
) # fallback if one wasn't defined in the preset
config_args = [CMake_EXE, "-S", directory]
if build_directory_path != None and len(build_directory_path) > 0:
config_args.append("-B")
config_args.append(build_directory_path)
# see cmake ide integration guide
if (
"entries" in data and data["entries"] and len(data["entries"]) > 0
) or "cacheVariables" in cmake_config_obj:
# initial_cache_path = norm_path_fslash(os.path.join(directory, "10x_initial_cache.json"))
# write_json_file(initial_cache_path, data["entries"])
initial_cache_script_path = norm_path_fslash(
os.path.join(directory, "10x_initial_cache.cmake")
)
cmake_vars = {}
# cacheVariables gets filled out with variables via cmake_prep from commandline
# if "entries" in data and data["entries"] and len(data["entries"]) > 0:
# for cache_var in data["entries"]:
# cmake_vars[cache_var["name"]] = cache_var
if "cacheVariables" in cmake_config_obj:
for k in cmake_config_obj["cacheVariables"]:
cmake_vars[k] = {
"name": k,
"value": cmake_config_obj["cacheVariables"][k],
}
with open(initial_cache_script_path, "w") as cache_script:
for k in cmake_vars:
if "type" in cmake_vars[k]:
cache_script.write(
'set({0} "{1}" CACHE {2} "")\n'.format(
k,
escape_bslash(cmake_vars[k]["value"]),
cmake_vars[k]["type"],
)
)
else:
cache_script.write(
'set({0} "{1}" CACHE STRING "")\n'.format(
k, escape_bslash(cmake_vars[k]["value"])
)
)
config_args.append("-C")
config_args.append(initial_cache_script_path)
config_args.append("--preset")
config_args.append(cmake_config_name)
if rebuild:
config_args.append("--fresh")
N10X.Editor.LogToBuildOutput(" ".join(config_args))
N10X.Editor.LogToBuildOutput("\n")
config_result = cmake_configure(config_args, directory)
build_dir = config_result["build_dir"]
if build_dir and len(build_dir):
if (
cmake_build_name != None
): # trust cmake's --preset command to build until we have full cmake parsing support
build_args = [CMake_EXE, "--build", "--preset", cmake_build_name]
else:
build_args = [CMake_EXE, "--build", build_dir]
N10X.Editor.LogToBuildOutput(" ".join(build_args))
N10X.Editor.LogToBuildOutput("\n")
exe_path = cmake_build(build_args, directory, build_dir)
elif settings_exists:
print("CMake Macro Expansion: ...")
# TODO detect and parse command line to pass into this function
data = cmake_prep(directory, None, [], False, True)
cmake_config_name = None
cmake_config_obj = {}
configs = []
if "configurations" in data:
configs = data["configurations"]
for config in configs:
if config["name"] == build_name:
cmake_config_name = config["name"]
cmake_config_obj = config
break
if cmake_config_name == None:
for config in configs:
if config["name"] == n10x_config:
cmake_config_name = config["name"]
cmake_config_obj = config
break
# try to find case insensitive versions as fallback
if cmake_config_name == None:
low_build_name = build_name.lower()
for config in configs:
if config["name"] == low_build_name:
cmake_config_name = config["name"]
cmake_config_obj = config
break
if cmake_config_name == None:
low_n10x_config = n10x_config.lower()
for config in configs:
if config["name"] == low_n10x_config:
cmake_config_name = config["name"]
cmake_config_obj = config
break
# TODO? Nearest config/preset name (did you mean) feedback?
if cmake_config_name == None:
N10X.Editor.LogToBuildOutput(
"Failed to find configuration in CMakeSettings.json for: {0} and {1}\n".format(
build_name, n10x_config
)
)
N10X.Editor.OnBuildFinished(False)
return True
cmake_macros = {}
if "macros" in data:
cmake_macros = data["macros"]
cmake_macros["${name}"] = cmake_config_name
# update the config and update by expanding presetName
cmake_config_obj = macro_expand_any(cmake_config_obj, cmake_macros)
if verbose:
print(json.dumps(cmake_config_obj, indent="\t"))
build_directory_path = None
if "buildRoot" in cmake_config_obj:
build_directory_path = cmake_config_obj["buildRoot"]
elif cmake_config_name != None:
build_directory_path = norm_path_fslash(
os.path.join(directory, "out\\build", cmake_config_name)
)
# TODO: read CMakeSettings.json? get --Config setting?
# "-S", directory, "-B", build_directory_path,
config_args = [CMake_EXE, "-S", directory, "-B", build_directory_path]
if "generator" in cmake_config_obj:
config_args.append("-G")
config_args.append(cmake_config_obj["generator"])
if (
"entries" in data and data["entries"] and len(data["entries"]) > 0
) or "cacheVariables" in cmake_config_obj:
initial_cache_script_path = norm_path_fslash(
os.path.join(directory, "10x_initial_cache.cmake")
)
cmake_vars = {}
if "entries" in data and data["entries"] and len(data["entries"]) > 0:
for cache_var in data["entries"]:
cmake_vars[cache_var["name"]] = cache_var["value"]
if "cacheVariables" in cmake_config_obj:
for k in cmake_config_obj["cacheVariables"]:
cmake_vars[k] = cmake_config_obj["cacheVariables"][k]
with open(initial_cache_script_path, "w") as cache_script:
for k in cmake_vars:
if "type" in cmake_vars[k]:
cache_script.write(
'set({0} "{1}" CACHE {2} "")\n'.format(
k,
escape_bslash(cmake_vars[k]["value"]),
cmake_vars[k]["type"],
)
)
else:
cache_script.write(
'set({0} "{1}" CACHE STRING "")\n'.format(
k, escape_bslash(cmake_vars[k]["value"])
)
)
config_args.append("-C") # see cmake ide integration guide
config_args.append(initial_cache_script_path)
if rebuild:
config_args.append("--fresh")
N10X.Editor.LogToBuildOutput(" ".join(config_args))
N10X.Editor.LogToBuildOutput("\n")
build_dir = cmake_configure(config_args, directory) # --Config
if build_dir and len(build_dir):
build_args = [
CMake_EXE,
"--build",
build_dir,
"--config",
cmake_config_name,
]
N10X.Editor.LogToBuildOutput(" ".join(build_args))
N10X.Editor.LogToBuildOutput("\n")
exe_path = cmake_build(build_args, directory, build_dir)
else:
# TODO detect and parse command line to pass into this function
data = cmake_prep(directory, None, [], False, False)
if verbose:
print(json.dumps(data, indent="\t"))
config_args = [CMake_EXE, "-S", directory, "-B", build_directory_path]
if "entries" in data and data["entries"] and len(data["entries"]) > 0:
initial_cache_path = norm_path_fslash(
os.path.join(directory, "10x_initial_cache.json")
)
write_json_file(initial_cache_path, data["entries"])
initial_cache_script_path = norm_path_fslash(
os.path.join(directory, "10x_initial_cache.cmake")
)
cmake_vars = {}
for cache_var in data["entries"]:
cmake_vars[cache_var["name"]] = cache_var["value"]
with open(initial_cache_script_path, "w") as cache_script:
for k in cmake_vars:
cache_script.write("set({0} CACHE {1}".format(k, cmake_vars[k]))
build_args.append("-C") # see cmake ide integration guide
build_args.append(initial_cache_script_path)
if rebuild:
config_args.append("--fresh")
N10X.Editor.LogToBuildOutput(" ".join(config_args))
N10X.Editor.LogToBuildOutput("\n")
build_dir = cmake_configure(config_args, directory)
if build_dir and len(build_dir):
build_args = [CMake_EXE, "--build", "."]
N10X.Editor.LogToBuildOutput(" ".join(build_args))
N10X.Editor.LogToBuildOutput("\n")
exe_path = cmake_build(build_args, directory, build_dir)
N10X.Editor.LogToBuildOutput("----- CMake Build Complete -----\n")
N10X.Editor.OnBuildFinished(True)
return True # intercept build command
def CMakeBuildStarted(filename: str, rebuild: bool = False):
cmake_hook_build_setting = N10X.Editor.GetSetting("CMake.HookBuild")
if cmake_hook_build_setting and cmake_hook_build_setting.lower() == "true":
pass
else:
print("CMake: ignoring build command (set CMake.HookBuild to true)")
return False
verbose_setting = N10X.Editor.GetSetting("CMake.Verbose")
verbose = verbose_setting and verbose_setting.lower()
CMake_EXE = N10X.Editor.GetSetting("CMake.Path").strip()
if not CMake_EXE:
CMake_EXE = "cmake"
if os.path.isdir(CMake_EXE):
CMake_EXE = os.path.join(CMake_EXE, "cmake.exe")
txworkspace = N10X.Editor.GetWorkspaceFilename()
directory, filename = split(txworkspace) # os.path.split
cmakelists_exists = IsCMakeDirectory(directory)
presetpath = norm_path_fslash(os.path.join(directory, "CMakePresets.json"))
userpresetpath = presetpath[: -len("CMakePresets.json")] + "CMakeUserPresets.json"
settingspath = presetpath[: -len("CMakePresets.json")] + "CMakeSettings.json"
listspath = presetpath[: -len("CMakePresets.json")] + "CMakeLists.txt"
preset_exists = exists(presetpath)
user_preset_exists = exists(userpresetpath)
settings_exists = exists(settingspath)
build_dir = None
print("Checking for CMake build...")
if cmakelists_exists:
version = cmake_version()
use_threads = True
if use_threads:
build_thread = Thread(
target=CMakeBuildThreaded,
args=[
{
"directory": directory,
"preset_exists": preset_exists,
"user_preset_exists": user_preset_exists,
"settings_exists": settings_exists,
"version": version,
"config": N10X.Editor.GetWorkspaceBuildConfig(),
"platform": N10X.Editor.GetWorkspaceBuildPlatform(),
"verbose": verbose,
"cmake": CMake_EXE,
"rebuild": rebuild,
}
],
)
build_thread.start()
else:
CMakeBuildThreaded(
{
"directory": directory,
"preset_exists": preset_exists,
"user_preset_exists": user_preset_exists,
"settings_exists": settings_exists,
"version": version,
"config": N10X.Editor.GetWorkspaceBuildConfig(),
"platform": N10X.Editor.GetWorkspaceBuildPlatform(),
"verbose": verbose,
"cmake": CMake_EXE,
"rebuild": rebuild,
}
)
return True
else:
print("No CMake related files found...")
N10X.Editor.OnBuildFinished(False)
return False
def OnCMakeRebuildStarted(filename: str) -> bool:
return CMakeBuildStarted(filename, True)
def OnCMakeBuildStarted(filename: str) -> bool:
return CMakeBuildStarted(filename, False)
def OnCMakeBuildFinished(build_result: bool):
N10X.Editor.LogToBuildOutput(
"----- CMake OK -----\n" if build_result else "----- CMake FAIL -----\n"
)
return
def IsOldWorkspace() -> bool:
build_cmd = N10X.Editor.GetWorkspaceSetting("BuildCommand")
rebuild_cmd = N10X.Editor.GetWorkspaceSetting("RebuildCommand")
build_file_cmd = N10X.Editor.GetWorkspaceSetting("BuildFileCommand")
clean_cmd = N10X.Editor.GetWorkspaceSetting("CleanCommand")
build_working_dir_cmd = N10X.Editor.GetWorkspaceSetting("BuildWorkingDirectory")
cancel_build_cmd = N10X.Editor.GetWorkspaceSetting("CancelBuild")
run_command_cmd = N10X.Editor.GetWorkspaceSetting("RunCommand")
run_working_directory_cmd = N10X.Editor.GetWorkspaceSetting("RunWorkingDirectory")
debug_cmd = N10X.Editor.GetWorkspaceSetting("DebugCommand")
exe_path_path = N10X.Editor.GetWorkspaceSetting("ExePath")
debug_sln_path = N10X.Editor.GetWorkspaceSetting("DebugSln")
return (
len(build_cmd) > 0
or len(rebuild_cmd) > 0
or len(build_file_cmd) > 0
or len(clean_cmd) > 0
or len(build_working_dir_cmd) > 0
or len(cancel_build_cmd) > 0
or len(run_command_cmd) > 0
or len(run_working_directory_cmd) > 0
or len(debug_cmd) > 0
or len(exe_path_path) > 0
or len(debug_sln_path) > 0
)
def cmake_condition(preset_configs: list) -> list:
non_hidden_configs = []
for preset in preset_configs:
if "hidden" in preset and preset["hidden"] == "true":
continue
if "condition" in preset:
condition = preset["condition"]
if condition["type"] is str and condition["type"].lower() == "equals":
if condition["lhs"] != condition["rhs"]:
continue
if condition["type"] is str and condition["type"].lower() == "notEquals":
if condition["lhs"] == condition["rhs"]:
continue
# TODO: other conditions
non_hidden_configs.append(preset)
return non_hidden_configs
def OnCMakeWorkspaceOpened():
cmake_hook_workspace_opened_setting = N10X.Editor.GetSetting(
"CMake.HookWorkspaceOpened"
)
if (
cmake_hook_workspace_opened_setting
and cmake_hook_workspace_opened_setting.lower() == "true"
):
pass
else:
print(
"CMake: ignoring workspace opened (set CMake.HookWorkspaceOpened == 'true'"
)
return False
verbose_setting = N10X.Editor.GetSetting("CMake.Verbose")
verbose = verbose_setting and verbose_setting.lower()
cmakelists_exists = False
cmakepresets_exists = False
cmakeuserpresets_exists = False
cmakesettings_exists = False
cmakeprojects = {}
# working from current project
txworkspace = N10X.Editor.GetWorkspaceFilename()
directory, filename = split(txworkspace) # os.path.split
cmakelists_exists = IsCMakeDirectory(directory)
presetpath = norm_path_fslash(os.path.join(directory, "CMakePresets.json"))
userpresetpath = presetpath[: -len("CMakePresets.json")] + "CMakeUserPresets.json"
settingspath = presetpath[: -len("CMakePresets.json")] + "CMakeSettings.json"
listspath = presetpath[: -len("CMakePresets.json")] + "CMakeLists.txt"
print("Working Directory uses CMake?: {0}".format(cmakelists_exists))
print("CMakeLists.txt: {0}".format(listspath))
preset_exists = exists(presetpath)
cmakeuserpresets_exists = exists(userpresetpath)
settings_exists = exists(settingspath)
build_cmd = N10X.Editor.GetWorkspaceSetting("BuildCommand")
rebuild_cmd = N10X.Editor.GetWorkspaceSetting("RebuildCommand")
build_file_cmd = N10X.Editor.GetWorkspaceSetting("BuildFileCommand")
clean_cmd = N10X.Editor.GetWorkspaceSetting("CleanCommand")
build_working_dir_cmd = N10X.Editor.GetWorkspaceSetting("BuildWorkingDirectory")
cancel_build_cmd = N10X.Editor.GetWorkspaceSetting("CancelBuild")
run_command_cmd = N10X.Editor.GetWorkspaceSetting("RunCommand")
run_working_directory_cmd = N10X.Editor.GetWorkspaceSetting("RunWorkingDirectory")
debug_cmd = N10X.Editor.GetWorkspaceSetting("DebugCommand")
exe_path_path = N10X.Editor.GetWorkspaceSetting("ExePath")
debug_sln_path = N10X.Editor.GetWorkspaceSetting("DebugSln")
cmake_preset_build = 'cmake --preset "$(Platform)-$(Configuration)" && cmake --build --preset "$(Platform)-$(Configuration)"'
cmake_preset_rebuild = 'cmake --preset "$(Platform)-$(Configuration)" --fresh && cmake --build --preset "$(Platform)-$(Configuration)"'
cmake_preset_run = "$(RootWorkspaceDirectory)/out/build/$(Platform)-$(Configuration)/project_name.exe"
cmake_preset_debug = "$(RootWorkspaceDirectory)/out/build/$(Platform)-$(Configuration)/project_name.exe"
cmake_settings_build = 'cmake -S "$(RootWorkspaceDirectory)" -B "$(RootWorkspaceDirectory)/out/build/$(Platform)-$(Configuration)" && cmake --build --config "$(Configuration)"'
cmake_settings_rebuild = 'cmake -S "$(RootWorkspaceDirectory)" -B "$(RootWorkspaceDirectory)/out/build/$(Platform)-$(Configuration)" --fresh && cmake --build --config "$(Configuration)"'
cmake_settings_run = "$(RootWorkspaceDirectory)/out/build/#(Platform)-$(Configuration)/project_name.exe"
cmake_settings_debug = "$(RootWorkspaceDirectory)/out/build/#(Platform)-$(Configuration)/project_name.exe"
cmake_empty_build = 'cmake -S "$(RootWorkspaceDirectory)" -B "$(RootWorkspaceDirectory)/out/build/$(Configuration)" && cmake --build -S "$(RootWorkspaceDirectory)" -B "$(RootWorkspaceDirectory)/out/build/$(Configuration)"'
cmake_empty_rebuild = 'cmake -S "$(RootWorkspaceDirectory)" -B "$(RootWorkspaceDirectory)/out/build/$(Configuration)" --fresh && cmake --build -S "$(RootWorkspaceDirectory)" -B "$(RootWorkspaceDirectory)/out/build/$(Configuration)"'
cmake_empty_run = (
"$(RootWorkspaceDirectory)/out/build/$(Configuration)/project_name.exe"
)
cmake_empty_debug = (
"$(RootWorkspaceDirectory)/out/build/$(Configuration)/project_name.exe"
)
if IsOldWorkspace():
pass
else:
if cmakelists_exists:
version = cmake_version()
# print("Configuring CMake Workspace...")
# project_name = CMakeProjectName()
if (preset_exists or cmakeuserpresets_exists) and version["preset_support"]:
N10X.Editor.SetWorkspaceSetting("BuildCommand", cmake_preset_build)
N10X.Editor.SetWorkspaceSetting("RebuildCommand", cmake_preset_rebuild)
N10X.Editor.SetWorkspaceSetting("RunCommand", cmake_preset_run)
N10X.Editor.SetWorkspaceSetting("DebugCommand", cmake_preset_debug)
N10X.Editor.SetWorkspaceSetting("ExePath", cmake_preset_run)
elif settings_exists:
N10X.Editor.SetWorkspaceSetting("BuildCommand", cmake_settings_build)
N10X.Editor.SetWorkspaceSetting("RebuildCommand", cmake_settings_build)
N10X.Editor.SetWorkspaceSetting("RunCommand", cmake_settings_run)
N10X.Editor.SetWorkspaceSetting("DebugCommand", cmake_settings_debug)
N10X.Editor.SetWorkspaceSetting("ExePath", cmake_settings_run)
else:
N10X.Editor.SetWorkspaceSetting("BuildCommand", cmake_empty_build)
N10X.Editor.SetWorkspaceSetting("RebuildCommand", cmake_empty_rebuild)
N10X.Editor.SetWorkspaceSetting("RunCommand", cmake_empty_run)
N10X.Editor.SetWorkspaceSetting("DebugCommand", cmake_empty_debug)
N10X.Editor.SetWorkspaceSetting("ExePath", cmake_empty_run)
# scan through workspace files for other directories
project_files = N10X.Editor.GetWorkspaceFiles()
for project_file in project_files:
if project_file.endswith("CMakeLists.txt"):
cmakelists_exists = True
cmakefolder = project_file[: -len("CMakeLists.txt")]
presetpath = cmakefolder + "CMakePresets.json"
userpresetpath = cmakefolder + "CMakeUserPresets.json"
settingspath = cmakefolder + "CMakeSettings.json"
preset_exists = exists(presetpath)
settings_exists = exists(settingspath)
user_preset_exists = exists(userpresetpath)
cmakepresets_exists = cmakepresets_exists or preset_exists
cmakesettings_exists = cmakesettings_exists or settings_exists
cmakeprojects[project_file] = {preset_exists, cmakesettings_exists}
if (preset_exists or user_preset_exists) and version["preset_support"]:
data = cmake_prep(
cmakefolder,
"$(RootWorkspaceDirectory)/out/build/$(Platform)-$(Configuration)",
[],
True,
False,
)
if verbose:
print(data)
# data = read_json_file(presetpath)
preset_version = data["version"]
preset_configs = data["configurePresets"]
build_configs = data["buildPresets"]
non_hidden_configs = cmake_condition(preset_configs)
non_hidden_build_configs = cmake_condition(build_configs)
build_list_results = []
for build_preset in non_hidden_configs:
if not build_preset["name"] in build_list_results:
build_list_results.append(build_preset["name"])
for build_preset in non_hidden_build_configs:
if not build_preset["name"] in build_list_results:
build_list_results.append(build_preset["name"])
workspace_file = norm_path_fslash(
os.path.join(cmakefolder, "cmakepreset.10x")
)
write10xWorkspace(
workspace_file,
cmake_preset_build,
cmake_preset_rebuild,
"",
"",
"",
"",
cmake_preset_run,
"",
cmake_preset_debug,
cmake_preset_run,
"",
build_list_results,
[],
)
elif settings_exists:
# TODO: parse json from CMakeSettings.json
# data = read_json_file(settingspath)
data = cmake_prep(
cmakefolder,
"$(RootWorkspaceDirectory)/out/build/$(Configuration)",
[],
False,
True,
)
if verbose:
print(data)
configuration_list = data["configurations"]
configurations = []
for json_object in configuration_list:
configurations.append(json_object["name"])
# print("Writing "+cmakefolder+"cmakesettings.10x")
workspace_file = norm_path_fslash(
os.path.join(cmakefolder, "cmakesettings.10x")
)
write10xWorkspace(
workspace_file,
cmake_settings_build,
cmake_settings_rebuild,
"",
"",
"",
"",
cmake_settings_run,
"",
cmake_settings_debug,
cmake_settings_run,
"",
configurations,
[],
)
else:
# print("Writing "+cmakefolder+"cmake.10x")
workspace_file = norm_path_fslash(
os.path.join(cmakefolder, "cmake.10x")
)
write10xWorkspace(
workspace_file,
cmake_empty_build,
cmake_empty_rebuild,
"",
"",
"",
"",
cmake_empty_run,
"",
cmake_empty_debug,
cmake_empty_run,
"",
[],
[],
)
def InitializeCMake():
N10X.Editor.AddOnWorkspaceOpenedFunction(OnCMakeWorkspaceOpened)
N10X.Editor.AddProjectBuildFunction(OnCMakeBuildStarted)
N10X.Editor.AddProjectRebuildFunction(OnCMakeRebuildStarted)
N10X.Editor.AddBuildFinishedFunction(OnCMakeBuildFinished)
N10X.Editor.CallOnMainThread(InitializeCMake)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment