1#!/usr/bin/env python3 2import json 3import argparse 4import os 5import pathlib 6import xml.etree.ElementTree as ET 7import uuid 8 9# This scripts takes a compile_commands.json file that was generated using `make -C tests/unit cmds_json` 10# and creates project files for an IDE that can be used for debugging user-space unit-tests 11# The project is not able to build XNU or the test executable 12 13SRC_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")) 14 15TESTS_UNIT_PREFIX = "tests/unit/" 16TESTS_UNIT_BUILD_PREFIX = TESTS_UNIT_PREFIX + "build/sym/" 17 18def parse_command(entry): 19 file = entry['file'] 20 directory = entry["directory"] 21 if not file.startswith(SRC_ROOT): 22 full_file = directory + "/" + file 23 else: 24 full_file = file 25 assert full_file.startswith(SRC_ROOT), "unexpected path" + full_file 26 rel_file = full_file[len(SRC_ROOT)+1:] 27 28 # arguments[0] is clang 29 args = entry['arguments'][1:] 30 31 args.extend(['-I', directory]) 32 return rel_file, args 33 34# -------------------------------------- Xcode project ---------------------------------------- 35# an Xcode project is a plist with a list of objects. each object has an ID and objects reference 36# each other by their ID. 37 38def do_quote_lst(dash_split): 39 output = [] 40 # change ' -DX=y z' to ' -DX="y z"' 41 for i, s in enumerate(dash_split): 42 if i == 0: 43 continue # skip the clang executable 44 if '=' in s: 45 st = s.strip() 46 eq_sp = st.split('=') 47 if ' ' in eq_sp[1]: 48 output.append(f'{eq_sp[0]}=\\"{eq_sp[1]}\\"') 49 continue 50 51 output.append(f"{s}") 52 return " ".join(output) 53 54class ObjType: 55 def __init__(self, idprefix, type_name): 56 self.type_name = type_name 57 self.id_prefix = idprefix 58 self.next_count = 1 59 def make_id(self): 60 id = f"{self.id_prefix:016d}{self.next_count:08d}" 61 self.next_count += 1 62 return id 63 64class ObjRegistry: 65 def __init__(self): 66 self.types = {} # map type-name to id-prefix (12 chars) 67 self.next_type_prefix = 1 68 69 self.objects = {} # map object-id to instance 70 71 def register(self, type_name, obj): 72 if type_name not in self.types: 73 self.types[type_name] = ObjType(self.next_type_prefix, type_name) 74 self.next_type_prefix += 1 75 id = self.types[type_name].make_id() 76 self.objects[id] = obj 77 return id 78 79 80obj_reg = ObjRegistry() 81 82TYPE_SOURCE_C = "sourcecode.c.c" 83TYPE_SOURCE_CPP = "sourcecode.cpp.cpp" 84TYPE_SOURCE_ASM = "sourcecode.asm" 85TYPE_HEADER = "sourcecode.c.h" 86TYPE_STATIC_LIB = "archive.ar" 87TYPE_EXE = '"compiled.mach-o.executable"' 88 89class ObjList: 90 def __init__(self, name=None): 91 self.name = name 92 self.objs = [] 93 def add(self, obj): 94 self.objs.append(obj) 95 def extend(self, lst): 96 self.objs.extend(lst) 97 98def tab(count): 99 return '\t' * count 100 101# The top-level object list is special in that it's grouped by the type of objects 102# This class represents part of the top level objects list 103class TopObjList(ObjList): 104 def write(self, out, lvl): 105 out.write(f"/* Begin {self.name} section */\n") 106 for obj in self.objs: 107 out.write(f"{tab(lvl)}{obj.id} = ") 108 obj.write(out, lvl) 109 out.write(f"/* End {self.name} section */\n\n") 110 111# a property that is serilized as a list of ids 112class IdList(ObjList): 113 def write(self, out, lvl): 114 out.write("(\n") # after = 115 for obj in self.objs: 116 out.write(f"{tab(lvl+1)}{obj.id} /* {obj.name} */,\n") 117 out.write(f"{tab(lvl)});\n") 118 119class StrList: 120 def __init__(self, lst): 121 self.lst = lst 122 def write(self, out, lvl): 123 out.write("(\n") # after = 124 for v in self.lst: 125 out.write(f"{tab(lvl+1)}{v},\n") 126 out.write(f"{tab(lvl)});\n") 127 @classmethod 128 def list_sort_quote(cls, s): 129 l = list(s) 130 l.sort() 131 return cls([f'"{d}"' for d in l]) 132 133class StrEval: 134 def __init__(self, fn): 135 self.fn = fn 136 def write(self, out, lvl): 137 out.write(self.fn() + ";\n") 138class LateEval: 139 def __init__(self, fn): 140 self.fn = fn 141 def write(self, out, lvl): 142 self.fn().write(out, lvl) 143 144class PDict: 145 def __init__(self, isa, inline=False): 146 self.d = {} 147 self.p = [] 148 self.inline = inline 149 if isa is not None: 150 self.isa = self.padd("isa", isa) 151 152 def padd(self, k, v, comment=None): 153 self.p.append((k, v, comment)) 154 self.d[k] = v 155 return v 156 def pextend(self, d): 157 for k, v in d.items(): 158 self.padd(k, v) 159 160 def write(self, out, lvl): 161 if self.inline: 162 out.write("{") 163 for k, v, comment in self.p: 164 assert isinstance(v, str) or isinstance(v, int), "complex value inline" 165 out.write(f"{k} = ") 166 if comment is None: 167 out.write(f"{v}; ") 168 else: 169 out.write(f"{v} /* {comment} */; ") 170 out.write("};\n") 171 else: 172 out.write("{\n") # comes after = 173 for k, v, comment in self.p: 174 out.write(f"{tab(lvl+1)}{k} = ") 175 if isinstance(v, str) or isinstance(v, int): 176 if comment is None: 177 out.write(f"{v};\n") 178 else: 179 out.write(f"{v} /* {comment} */;\n") 180 else: 181 v.write(out, lvl+1) 182 out.write(f"{tab(lvl)}}};\n") 183 184 185class File: 186 def __init__(self, name, args): 187 self.name = name.split('/')[-1] 188 self.args = args 189 self.ref = None 190 191 def type_str(self): 192 ext = os.path.splitext(self.name)[1] 193 if ext == ".c": 194 return TYPE_SOURCE_C 195 if ext == ".h": 196 return TYPE_HEADER 197 if ext == ".cpp": 198 return TYPE_SOURCE_CPP 199 if ext == ".a": 200 return TYPE_STATIC_LIB 201 if ext == ".s": 202 return TYPE_SOURCE_ASM 203 if ext == '': 204 return TYPE_EXE 205 return None 206 207class BuildFile(PDict): 208 def __init__(self, file): 209 PDict.__init__(self, "PBXBuildFile", inline=True) 210 self.id = obj_reg.register("build_file", self) 211 self.file = file 212 self.name = file.name 213 self.padd("fileRef", self.file.ref.id, comment=self.file.name) 214 215class FileRef(PDict): 216 def __init__(self, file): 217 PDict.__init__(self, "PBXFileReference", inline=True) 218 self.id = obj_reg.register("file_ref", self) 219 self.file = file 220 file.ref = self 221 typ = self.file.type_str() 222 assert typ is not None, "unknown file type " + self.file.name 223 if typ == TYPE_STATIC_LIB or typ == TYPE_EXE: 224 self.padd("explicitFileType", typ) 225 self.padd("includeInIndex", 0) 226 self.padd("path", f'"{self.file.name}"') 227 self.padd("sourceTree", "BUILT_PRODUCTS_DIR") 228 else: 229 self.padd("lastKnownFileType", typ) 230 self.padd("path", f'"{self.file.name}"') 231 self.padd("sourceTree", '"<group>"') 232 233 @property 234 def name(self): 235 return self.file.name 236 237class Group(PDict): 238 def __init__(self, name=None, path=None): 239 PDict.__init__(self, "PBXGroup") 240 self.id = obj_reg.register("group", self) 241 self.children = self.padd("children", IdList()) 242 self.child_dict = {} # map name to Group/FileRef 243 if name is not None: 244 self.name = self.padd("name", name) 245 if path is not None: 246 self.name = self.padd("path", f'"{path}"') 247 self.padd("sourceTree", '"<group>"') 248 249 def rec_add(self, sp_path, groups_lst, file_ref): 250 elem = sp_path[0] 251 if len(sp_path) == 1: 252 assert elem not in self.child_dict, f"already have file elem {elem} in {self.name}" 253 self.children.add(file_ref) 254 self.child_dict[elem] = file_ref 255 #file_ref.file.name = elem # remove the path from the name 256 else: 257 if elem in self.child_dict: 258 g = self.child_dict[elem] 259 else: 260 g = Group(path=elem) 261 groups_lst.add(g) 262 self.children.add(g) 263 self.child_dict[elem] = g 264 g.rec_add(sp_path[1:], groups_lst, file_ref) 265 266 def sort(self): 267 self.children.objs.sort(key=lambda x: x.name) 268 for elem in self.children.objs: 269 if isinstance(elem, Group): 270 elem.sort() 271 272class BuildPhase(PDict): 273 def __init__(self, isa, name): 274 PDict.__init__(self, isa) 275 self.id = obj_reg.register("build_phase", self) 276 self.name = name 277 self.padd("buildActionMask", 2147483647) 278 self.files = self.padd("files", IdList()) 279 self.padd("runOnlyForDeploymentPostprocessing", 0) 280 281class Target(PDict): 282 def __init__(self, name, file_ref, cfg_lst, prod_type): 283 PDict.__init__(self, "PBXNativeTarget") 284 self.id = obj_reg.register("target", self) 285 self.cfg_lst = self.padd("buildConfigurationList", cfg_lst.id) 286 self.build_phases = self.padd("buildPhases", IdList()) 287 self.padd("buildRules", IdList()) 288 self.padd("dependencies", IdList()) 289 self.name = self.padd("name", name) 290 self.padd("packageProductDependencies", IdList()) 291 self.padd("productName", name) 292 self.padd("productReference", file_ref.id, comment=file_ref.name) 293 self.padd("productType", prod_type) 294 295class CfgList(PDict): 296 def __init__(self, name): 297 PDict.__init__(self, "XCConfigurationList") 298 self.id = obj_reg.register("config_list", self) 299 self.name = name # not used 300 self.configs = self.padd("buildConfigurations", IdList()) 301 self.padd("defaultConfigurationIsVisible", 0) 302 self.padd("defaultConfigurationName", StrEval(lambda: self.configs.objs[0].name)) 303 304class Config(PDict): 305 def __init__(self, name): 306 PDict.__init__(self, "XCBuildConfiguration") 307 self.id = obj_reg.register("config", self) 308 self.settings = self.padd("buildSettings", PDict(None)) 309 self.name = self.padd("name", name) 310 311class Project(PDict): 312 def __init__(self, cfg_lst, group_main, group_prod): 313 PDict.__init__(self, "PBXProject") 314 self.id = obj_reg.register("project", self) 315 self.targets = IdList("targets") 316 self.padd("attributes", LateEval(lambda: self.make_attr())) 317 self.padd("buildConfigurationList", cfg_lst.id, comment=cfg_lst.name) 318 self.padd("developmentRegion", "en") 319 self.padd("hasScannedForEncodings", "0") 320 self.padd("knownRegions", StrList(["en", "Base"])) 321 self.padd("mainGroup", group_main.id) 322 self.padd("minimizedProjectReferenceProxies", "1") 323 self.padd("preferredProjectObjectVersion", "77") 324 self.padd("productRefGroup", group_prod.id) 325 self.padd("projectDirPath", '""') 326 self.padd("projectRoot", '""') 327 self.padd("targets", self.targets) 328 329 def make_attr(self): 330 a = PDict(None) 331 a.padd("BuildIndependentTargetsInParallel", 1) 332 a.padd("LastUpgradeCheck", 1700) 333 ta = a.padd("TargetAttributes", PDict(None)) 334 for t in self.targets.objs: 335 p = ta.padd(t.id, PDict(None)) 336 p.padd("CreatedOnToolsVersion", "17.0") 337 return a 338 339 340class PbxProj: 341 def __init__(self): 342 self.top_obj = [] 343 self.build_files = self.add_top(TopObjList("PBXBuildFile")) 344 self.file_refs = self.add_top(TopObjList("PBXFileReference")) 345 self.groups = self.add_top(TopObjList("PBXGroup")) 346 self.build_phases = self.add_top(TopObjList("build phases")) 347 self.targets = self.add_top(TopObjList("PBXNativeTarget")) 348 self.projects = self.add_top(TopObjList("PBXProject")) 349 self.configs = self.add_top(TopObjList("XCBuildConfiguration")) 350 self.config_lists = self.add_top(TopObjList("XCConfigurationList")) 351 352 self.group_main = self.add_group(Group()) 353 self.group_products = self.add_group(Group(name="Products")) 354 self.group_main.children.add(self.group_products) 355 356 self.cfg_prod_release = self.add_config(Config("Release")) 357 self.cfg_prod_release.settings.pextend({"SDKROOT": "macosx", 358 "MACOSX_DEPLOYMENT_TARGET": "14.1", 359 }) 360 self.proj_cfg_lst = self.add_cfg_lst(CfgList("proj config list")) 361 self.proj_cfg_lst.configs.add(self.cfg_prod_release) 362 363 self.root_proj = Project(self.proj_cfg_lst, self.group_main, self.group_products) 364 self.projects.add(self.root_proj) 365 366 self.test_exec = [] 367 368 def add_top(self, t): 369 self.top_obj.append(t) 370 return t 371 def add_group(self, g): 372 self.groups.add(g) 373 return g 374 def add_build_phase(self, p): 375 self.build_phases.add(p) 376 return p 377 def add_config(self, c): 378 self.configs.add(c) 379 return c 380 def add_cfg_lst(self, c): 381 self.config_lists.add(c) 382 return c 383 def add_target(self, t): 384 self.targets.add(t) 385 return t 386 387 def add_xnu_archive(self): 388 f = File("libkernel.a", []) 389 fr = FileRef(f) 390 self.file_refs.add(fr) 391 self.group_products.children.add(fr) 392 self.xnu_phase_headers = self.add_build_phase(BuildPhase("PBXHeadersBuildPhase", "Headers")) 393 self.xnu_phase_sources = self.add_build_phase(BuildPhase("PBXSourcesBuildPhase", "Sources")) 394 395 cfg_xnu_release = self.add_config(Config("Release")) 396 cfg_xnu_release.settings.pextend( { "CODE_SIGN_STYLE": "Automatic", 397 "EXECUTABLE_PREFIX": "lib", 398 "PRODUCT_NAME": '"$(TARGET_NAME)"', 399 "SKIP_INSTALL": "YES"}) 400 xnu_cfg_lst = self.add_cfg_lst(CfgList("target config list")) 401 xnu_cfg_lst.configs.add(cfg_xnu_release) 402 403 target = self.add_target(Target("xnu_static_lib", fr, xnu_cfg_lst, '"com.apple.product-type.library.static"')) 404 target.build_phases.extend([self.xnu_phase_headers, self.xnu_phase_sources]) 405 self.root_proj.targets.add(target) 406 407 def add_test_target(self, c_file_ref, c_build_file): 408 name = os.path.splitext(os.path.split(c_file_ref.name)[1])[0] 409 f = File(name, []) 410 fr = FileRef(f) 411 self.file_refs.add(fr) 412 self.group_products.children.add(fr) 413 phase_h = self.add_build_phase(BuildPhase("PBXHeadersBuildPhase", "Headers")) 414 phase_src = self.add_build_phase(BuildPhase("PBXSourcesBuildPhase", "Sources")) 415 phase_src.files.add(c_build_file) 416 417 cfg_release = self.add_config(Config("Release")) 418 cfg_release.settings.pextend( { "CODE_SIGN_STYLE": "Automatic", 419 "PRODUCT_NAME": '"$(TARGET_NAME)"'}) 420 cfg_lst = self.add_cfg_lst(CfgList("target config list")) 421 cfg_lst.configs.add(cfg_release) 422 423 target = self.add_target(Target(name, fr, cfg_lst, '"com.apple.product-type.tool"')) 424 target.build_phases.extend([phase_h, phase_src]) 425 self.root_proj.targets.add(target) 426 self.test_exec.append(target) 427 428 def add_file(self, file_path, flags): 429 f = File(file_path, flags) 430 fr = FileRef(f) 431 bf = BuildFile(f) 432 self.build_files.add(bf) 433 self.file_refs.add(fr) 434 self.group_main.rec_add(file_path.split('/'), self.groups, fr) 435 typ = f.type_str() 436 if typ == TYPE_HEADER: 437 self.xnu_phase_headers.files.add(bf) 438 elif typ in [TYPE_SOURCE_C, TYPE_SOURCE_CPP, TYPE_SOURCE_ASM]: 439 self.xnu_phase_sources.files.add(bf) 440 return fr, bf 441 def add_ccj(self, ccj): 442 test_targets = [] 443 for entry in ccj: 444 src_file, flags = parse_command(entry) 445 if src_file.endswith('dt_proxy.c'): 446 continue 447 fr, bf = self.add_file(src_file, flags) 448 if src_file.startswith(TESTS_UNIT_PREFIX): 449 test_targets.append((fr, bf)) 450 test_targets.sort(key=lambda x:x[1].name) 451 for fr, bf in test_targets: 452 self.add_test_target(fr, bf) 453 454 def add_headers(self): 455 for path in pathlib.Path(SRC_ROOT).rglob('*.h'): 456 full_file = str(path) 457 assert full_file.startswith(SRC_ROOT), "unexpected path" + full_file 458 rel_file = full_file[len(SRC_ROOT)+1:] 459 self.add_file(str(rel_file), None) 460 461 def sort_groups(self): 462 self.group_main.sort() 463 464 def write(self, out): 465 out.write("// !$*UTF8*$!\n{\n") 466 out.write("\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 77;\n\tobjects = {\n\n") 467 for t in self.top_obj: 468 t.write(out, 2) 469 out.write(f"\t}};\n\trootObject = {self.root_proj.id};\n") 470 out.write("}") 471 472 def make_settings(self): 473 # go over all build files and find in their arguments a union of all the included folders 474 # this is useful for file navigation in xcode to work correctly 475 inc_dirs = set() 476 common_defines = None 477 for f in self.build_files.objs: 478 file_defines = set() 479 args = f.file.args 480 if args is None: 481 continue 482 for i, arg in enumerate(args): 483 if arg == '-I': 484 d = args[i + 1] 485 if d != ".": 486 inc_dirs.add(args[i + 1]) 487 elif arg == '-D': 488 file_defines.add(args[i+1]) 489 if common_defines is None: 490 common_defines = file_defines 491 else: 492 common_defines = common_defines.intersection(file_defines) 493 inc_str_lst = StrList.list_sort_quote(inc_dirs) 494 self.cfg_prod_release.settings.padd("HEADER_SEARCH_PATHS", inc_str_lst) 495 self.cfg_prod_release.settings.padd("SYSTEM_HEADER_SEARCH_PATHS", inc_str_lst) 496 str_common_defs = StrList.list_sort_quote(common_defines) 497 self.cfg_prod_release.settings.padd("GCC_PREPROCESSOR_DEFINITIONS", str_common_defs) 498 499 def write_schemes(self, folder, container_dir): 500 for target in self.test_exec: 501 path = os.path.join(folder, target.name + ".xcscheme") 502 out = open(path, "w") 503 exec_path = SRC_ROOT + "/" + TESTS_UNIT_BUILD_PREFIX + target.name 504 out.write(f'''<?xml version="1.0" encoding="UTF-8"?> 505<Scheme 506 LastUpgradeVersion = "1630" 507 version = "1.7"> 508 <BuildAction 509 parallelizeBuildables = "YES" 510 buildImplicitDependencies = "YES" 511 buildArchitectures = "Automatic"> 512 <BuildActionEntries> 513 <BuildActionEntry 514 buildForTesting = "NO" 515 buildForRunning = "NO" 516 buildForProfiling = "YES" 517 buildForArchiving = "NO" 518 buildForAnalyzing = "NO"> 519 <BuildableReference 520 BuildableIdentifier = "primary" 521 BlueprintIdentifier = "{target.id}" 522 BuildableName = "{target.name}" 523 BlueprintName = "{target.name}" 524 ReferencedContainer = "container:{container_dir}"> 525 </BuildableReference> 526 </BuildActionEntry> 527 </BuildActionEntries> 528 </BuildAction> 529 <LaunchAction 530 buildConfiguration = "Release" 531 selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" 532 selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" 533 launchStyle = "0" 534 useCustomWorkingDirectory = "NO" 535 ignoresPersistentStateOnLaunch = "NO" 536 debugDocumentVersioning = "YES" 537 debugServiceExtension = "internal" 538 allowLocationSimulation = "YES" 539 internalIOSLaunchStyle = "3" 540 viewDebuggingEnabled = "No"> 541 <PathRunnable 542 runnableDebuggingMode = "0" 543 FilePath = "{exec_path}"> 544 </PathRunnable> 545 <MacroExpansion> 546 <BuildableReference 547 BuildableIdentifier = "primary" 548 BlueprintIdentifier = "{target.id}" 549 BuildableName = "{target.name}" 550 BlueprintName = "{target.name}" 551 ReferencedContainer = "container:{container_dir}"> 552 </BuildableReference> 553 </MacroExpansion> 554 </LaunchAction> 555</Scheme> 556''') 557 print(f"Wrote {path}") 558 559def gen_xcode(ccj): 560 p = PbxProj() 561 p.add_xnu_archive() 562 p.add_ccj(ccj) 563 p.add_headers() 564 p.sort_groups() 565 p.make_settings() 566 567 output = os.path.join(SRC_ROOT, "ut_xnu_proj.xcodeproj") 568 os.makedirs(output, exist_ok=True) 569 proj_path = os.path.join(output, "project.pbxproj") 570 p.write(open(proj_path, "w")) 571 print(f'wrote file: {proj_path};') 572 573 schemes_dir = output + "/xcshareddata/xcschemes" 574 os.makedirs(schemes_dir, exist_ok=True) 575 p.write_schemes(schemes_dir, output) 576 print(f'wrote schemes to: {schemes_dir}') 577 578# -------------------------------------- VSCode launch targets ---------------------------------------- 579 580class TargetsProject: 581 def __init__(self): 582 self.targets = [] 583 584 def add_ccj(self, ccj): 585 for entry in ccj: 586 src_file, flags = parse_command(entry) 587 if src_file.startswith(TESTS_UNIT_PREFIX): 588 name = os.path.splitext(src_file[len(TESTS_UNIT_PREFIX):])[0] 589 self.targets.append(name) 590 self.targets.sort() 591 592class VsCodeLaunchJson(TargetsProject): 593 def write(self, f): 594 confs = [] 595 launch = {"version": "0.2.0", "configurations": confs } 596 for t in self.targets: 597 confs.append({ 598 "name": t, 599 "type": "lldb-dap", 600 "request": "launch", 601 "program": "${workspaceFolder}/" + TESTS_UNIT_BUILD_PREFIX + t, 602 "stopOnEntry": False, 603 "cwd": "${workspaceFolder}", 604 "args": [], 605 "env": [] 606 }) 607 json.dump(launch, f, indent=4) 608 609 610def gen_vscode(ccj): 611 p = VsCodeLaunchJson() 612 p.add_ccj(ccj) 613 614 output = os.path.join(SRC_ROOT, ".vscode/launch.json") 615 os.makedirs(os.path.join(SRC_ROOT, ".vscode"), exist_ok=True) 616 if os.path.exists(output): 617 print(f"deleting existing {output}") 618 os.unlink(output) 619 p.write(open(output, "w")) 620 print(f"wrote {output}") 621 622# -------------------------------------- CLion targets ---------------------------------------- 623 624def find_elem(root, tag, **kvarg): 625 assert len(kvarg.items()) == 1 626 key, val = list(kvarg.items())[0] 627 for child in root: 628 assert child.tag == tag, f'unexpected child.tag {child.tag}' 629 if child.attrib[key] == val: 630 return child 631 return None 632 633def get_elem(root, tag, **kvarg): 634 child = find_elem(root, tag, **kvarg) 635 key, val = list(kvarg.items())[0] 636 if child is not None: 637 return child, False 638 comp = ET.SubElement(root, tag) 639 comp.attrib[key] = val 640 return comp, True 641 642 643CLION_TOOLCHAIN_NAME = "System" 644class CLionProject(TargetsProject): 645 def _get_root(self, path): 646 if os.path.exists(path): 647 print(f"Parsing existing file {path}") 648 root = ET.parse(path).getroot() 649 assert root.tag == 'project', f'unexpected root.tag {root.tag}' 650 else: 651 root = ET.Element('project') 652 root.attrib["version"] = "4" 653 return root 654 655 def _write(self, root, path): 656 tree = ET.ElementTree(root) 657 ET.indent(tree, space=' ', level=0) 658 tree.write(open(path, "wb"), encoding="utf-8", xml_declaration=True) 659 print(f"Wrote {path}") 660 661 def make_custom_targets(self): 662 # add a target that uses toolchain "System" 663 path = os.path.join(SRC_ROOT, ".idea/customTargets.xml") 664 root = self._get_root(path) 665 comp, _ = get_elem(root, "component", name="CLionExternalBuildManager") 666 # check if we already have the target we need 667 for target in comp: 668 if target.attrib["defaultType"] == "TOOL": 669 target_name = target.attrib["name"] 670 if len(target) == 1 and target[0].tag == "configuration": 671 conf = target[0] 672 if conf.attrib["toolchainName"] == CLION_TOOLCHAIN_NAME: 673 conf_name = conf.attrib["name"] 674 print(f"file {path} already has the needed target with name {target_name},{conf_name}") 675 return target_name, conf_name # it already exists, nothing to do 676 # add a new target 677 target_name = "test_default" 678 conf_name = "test_default" 679 680 target = ET.SubElement(comp, "target") 681 target.attrib["id"] = str(uuid.uuid1()) 682 target.attrib["name"] = target_name 683 target.attrib["defaultType"] = "TOOL" 684 685 conf = ET.SubElement(target, "configuration") 686 conf.attrib["id"] = str(uuid.uuid1()) 687 conf.attrib["name"] = conf_name 688 conf.attrib["toolchainName"] = CLION_TOOLCHAIN_NAME 689 print(f"Created target named {target_name}") 690 self._write(root, path) 691 return target_name, conf_name 692 693 def add_to_workspace(self, target_name, conf_name): 694 path = os.path.join(SRC_ROOT, ".idea/workspace.xml") 695 root = self._get_root(path) 696 comp, _ = get_elem(root, "component", name="RunManager") 697 added_anything = False 698 for t in self.targets: 699 for conf in comp: 700 if conf.tag != "configuration": 701 continue 702 if conf.attrib["name"] == t: # already has this target 703 print(f"Found existing configuration named '{t}', not adding it") 704 break 705 else: 706 print(f"Adding configuration for '{t}'") 707 proj_name = os.path.basename(SRC_ROOT) 708 conf = ET.SubElement(comp, "configuration", name=t, 709 type="CLionExternalRunConfiguration", 710 factoryName="Application", 711 REDIRECT_INPUT="false", 712 ELEVATE="false", 713 USE_EXTERNAL_CONSOLE="false", 714 EMULATE_TERMINAL="false", 715 PASS_PARENT_ENVS_2="true", 716 PROJECT_NAME=proj_name, 717 TARGET_NAME=target_name, 718 CONFIG_NAME=conf_name, 719 RUN_PATH=f"$PROJECT_DIR$/{TESTS_UNIT_BUILD_PREFIX}{t}") 720 ET.SubElement(conf, "method", v="2") 721 added_anything = True 722 if added_anything: 723 self._write(root, path) 724 725 726def gen_clion(ccj): 727 p = CLionProject() 728 p.add_ccj(ccj) 729 730 os.makedirs(os.path.join(SRC_ROOT, ".idea"), exist_ok=True) 731 target_name, conf_name = p.make_custom_targets() 732 p.add_to_workspace(target_name, conf_name) 733 734 735def main(): 736 parser = argparse.ArgumentParser(description='Generate xcode project from compile_commands.json') 737 parser.add_argument('mode', help='IDE to generate for', choices=['xcode', 'vscode', 'clion']) 738 parser.add_argument('compile_commands', help='Path to compile_commands.json', nargs='*', default=os.path.join(SRC_ROOT, "compile_commands.json")) 739 args = parser.parse_args() 740 741 if not os.path.exists(args.compile_commands): 742 print(f"Can't find input {args.compile_commands}") 743 return 1 744 745 ccj = json.load(open(args.compile_commands, 'r')) 746 747 if args.mode == 'xcode': 748 return gen_xcode(ccj) 749 elif args.mode == 'vscode': 750 return gen_vscode(ccj) 751 elif args.mode == 'clion': 752 return gen_clion(ccj) 753 754 755if __name__ == '__main__': 756 main() 757 758